Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
49.34% covered (warning)
49.34%
150 / 304
22.22% covered (danger)
22.22%
2 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
Database
49.34% covered (warning)
49.34%
150 / 304
22.22% covered (danger)
22.22%
2 / 9
1482.16
0.00% covered (danger)
0.00%
0 / 1
 configure
72.87% covered (success)
72.87%
94 / 129
0.00% covered (danger)
0.00%
0 / 1
69.38
 test
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 seed
51.85% covered (warning)
51.85%
14 / 27
0.00% covered (danger)
0.00%
0 / 1
15.14
 export
0.00% covered (danger)
0.00%
0 / 30
0.00% covered (danger)
0.00%
0 / 1
90
 import
0.00% covered (danger)
0.00%
0 / 29
0.00% covered (danger)
0.00%
0 / 1
90
 createAdapter
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 install
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 reset
45.45% covered (warning)
45.45%
20 / 44
0.00% covered (danger)
0.00%
0 / 1
70.58
 clear
48.78% covered (warning)
48.78%
20 / 41
0.00% covered (danger)
0.00%
0 / 1
55.83
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\Kettle\Model;
15
16use Pop\Console\Console;
17use Pop\Console\Color;
18use Pop\Db\Db;
19use Pop\Db\Adapter;
20use Pop\Db\Sql\Seeder;
21use Pop\Model\AbstractModel;
22
23/**
24 * Database model class
25 *
26 * @category   Pop\Kettle
27 * @package    Pop\Kettle
28 * @author     Nick Sagona, III <dev@nolainteractive.com>
29 * @copyright  Copyright (c) 2009-2024 NOLA Interactive, LLC. (http://www.nolainteractive.com)
30 * @license    http://www.popphp.org/license     New BSD License
31 * @version    2.3.2
32 */
33class Database extends AbstractModel
34{
35
36    /**
37     * Configure database
38     *
39     * @param  Console $console
40     * @param  string  $location
41     * @param  string  $database
42     * @return Database
43     */
44    public function configure(Console $console, string $location, string $database = 'default'): Database
45    {
46        $dbUser     = '';
47        $dbPass     = '';
48        $dbHost     = '';
49        $realDbName = '';
50        $dbAdapters = Db::getAvailableAdapters();
51        $dbChoices  = [];
52        $i          = 1;
53
54        if (!file_exists($location . '/database')) {
55            mkdir($location . '/database');
56        }
57        if (!file_exists($location . '/database/migrations')) {
58            mkdir($location . '/database/migrations');
59        }
60        if (!file_exists($location . '/database/seeds')) {
61            mkdir($location . '/database/seeds');
62        }
63        if (!file_exists($location . '/database/snapshots')) {
64            mkdir($location . '/database/snapshots');
65        }
66        if (!file_exists($location . '/database/migrations/' . $database)) {
67            mkdir($location . '/database/migrations/' . $database);
68        }
69        if (!file_exists($location . '/database/seeds/' . $database)) {
70            mkdir($location . '/database/seeds/' . $database);
71        }
72        if (!file_exists($location . '/database/snapshots/' . $database)) {
73            mkdir($location . '/database/snapshots/' . $database);
74        }
75
76        foreach ($dbAdapters as $adapter => $result) {
77            if ($adapter == 'pdo') {
78                foreach ($result as $a => $r) {
79                    if ($r) {
80                        $console->write($i . ': PDO ' . str_replace(
81                            ['sql', 'Pg'], ['SQL', 'Postgre'], ucfirst($a))
82                        );
83                        $dbChoices[strtolower('pdo_' . $a)] = $i;
84                        $i++;
85                    }
86                }
87            } else if ($result) {
88                $console->write($i . ': ' . str_replace(
89                    ['sqli', 'sql', 'Pg'], ['SQL', 'SQL', 'Postgre'], ucfirst($adapter))
90                );
91                $dbChoices[strtolower(str_replace('ysqli', 'ysql', $adapter))] = $i;
92                $i++;
93            }
94        }
95
96        // For testing purposes
97        if (isset($_SERVER['X_POP_CONSOLE_INPUT_2'])) {
98            $_SERVER['X_POP_CONSOLE_INPUT'] = $_SERVER['X_POP_CONSOLE_INPUT_2'];
99        }
100
101        $console->write();
102        $adapter = $console->prompt('Please select one of the above database adapters: ', $dbChoices);
103        $console->write();
104
105        $dbAdapter = array_search($adapter, $dbChoices);
106        $sqliteDb  = null;
107
108        // If PDO
109        if (str_contains($dbAdapter, 'pdo')) {
110            $dbInterface = 'Pdo';
111            $dbType      = substr($dbAdapter, (strpos($dbAdapter, '_') + 1));
112        } else {
113            $dbInterface = ucfirst(strtolower($dbAdapter));
114            $dbType      = null;
115        }
116
117        // For testing purposes
118        if (isset($_SERVER['X_POP_CONSOLE_INPUT_3'])) {
119            $_SERVER['X_POP_CONSOLE_INPUT'] = $_SERVER['X_POP_CONSOLE_INPUT_3'];
120        }
121
122        if (($dbInterface == 'Sqlite') || ($dbType == 'sqlite')) {
123            $dbName     = $console->prompt('DB Name: ', null, true);
124            $sqliteFile = $dbName . ((!str_contains($dbName, '.sqlite')) ? '.sqlite' : '');
125            $sqliteFile = str_replace(' ', '_', $sqliteFile);
126
127            chmod($location . '/database', 0755);
128            touch($location . '/database/' . $sqliteFile);
129            chmod($location . '/database/' . $sqliteFile, 0777);
130
131            $sqliteDb = realpath($location . '/database/' . $sqliteFile);
132            $console->write();
133        } else {
134            $dbCheck = false;
135            while ($dbCheck !== true) {
136                $dbName     = ($database != 'default') ?
137                    $console->prompt('DB Name: [' . $database .']') : $console->prompt('DB Name: ', null, true);
138                $dbUser     = $console->prompt('DB User: ', null, true);
139                $dbPass     = $console->prompt('DB Password: ', null, true);
140                $dbHost     = $console->prompt('DB Host: [localhost] ', null, true);
141
142                if (($dbName == '') && ($database != 'default')) {
143                    $dbName = $database;
144                }
145                if ($dbHost == '') {
146                    $dbHost = 'localhost';
147                }
148
149                $realDbName = $dbName;
150
151                $dbCheck = Db::check($dbInterface, [
152                    'database' => $dbName,
153                    'username' => $dbUser,
154                    'password' => $dbPass,
155                    'host'     => $dbHost,
156                    'type'     => $dbType,
157                ]);
158
159                if ($dbCheck !== true) {
160                    $console->write();
161                    $console->write($console->colorize(
162                        'Database configuration test failed. Please try again. ' . PHP_EOL . PHP_EOL .
163                        '    ' . $dbCheck, Color::BOLD_RED
164                    ));
165                } else {
166                    $console->write();
167                    $console->write($console->colorize(
168                        'Database configuration test passed.', Color::BOLD_GREEN
169                    ));
170                }
171                $console->write();
172            }
173        }
174
175        $console->write('Writing database configuration file...');
176
177        if (!file_exists($location . '/app')) {
178            mkdir($location . '/app');
179        }
180        if (!file_exists($location . '/app/config')) {
181            mkdir($location . '/app/config');
182        }
183
184        if (!file_exists($location . DIRECTORY_SEPARATOR . '/app/config/database.php')) {
185            copy(
186                __DIR__ . '/../../config/templates/api/app/config/database.php',
187                $location . DIRECTORY_SEPARATOR . '/app/config/database.php'
188            );
189        }
190
191        if (!file_exists($location . DIRECTORY_SEPARATOR . '/.env')) {
192            copy(
193                __DIR__ . '/../../config/templates/orig.env',
194                $location . DIRECTORY_SEPARATOR . '/.env'
195            );
196        }
197
198        if ($sqliteDb !== null) {
199            $realDbName = $sqliteDb;
200        }
201        if (str_contains($realDbName, ' ') && !str_starts_with($realDbName, '"') && !str_ends_with($realDbName, '"')) {
202            $realDbName = '"' . $realDbName . '"';
203        }
204        if (str_contains($dbUser, ' ') && !str_starts_with($dbUser, '"') && !str_ends_with($dbUser, '"')) {
205            $dbUser = '"' . $dbUser . '"';
206        }
207        if (str_contains($dbPass, ' ') && !str_starts_with($dbPass, '"') && !str_ends_with($dbPass, '"')) {
208            $dbPass = '"' . $dbPass . '"';
209        }
210
211        $env = str_replace([
212            'DB_DATABASE=',
213            'DB_ADAPTER=',
214            'DB_USERNAME=',
215            'DB_PASSWORD=',
216            'DB_HOST=',
217            'DB_TYPE=',
218        ], [
219            'DB_DATABASE=' . $realDbName,
220            'DB_ADAPTER=' . strtolower($dbInterface),
221            'DB_USERNAME=' . $dbUser,
222            'DB_PASSWORD=' . $dbPass,
223            'DB_HOST=' . $dbHost,
224            'DB_TYPE=' . $dbType,
225        ], file_get_contents($location . DIRECTORY_SEPARATOR . '/.env'));
226
227        file_put_contents($location . DIRECTORY_SEPARATOR . '/.env', $env);
228
229        return $this;
230    }
231
232    /**
233     * Test database connection
234     *
235     * @param  array  $database
236     * @return string|bool
237     */
238    public function test(array $database): string|bool
239    {
240        return Db::check($database['adapter'], array_diff_key($database, array_flip(['adapter'])));
241    }
242
243    /**
244     * Seed database
245     *
246     * @param  Console $console
247     * @param  string  $location
248     * @param  string  $database
249     * @return Database
250     */
251    public function seed(Console $console, string $location, string $database = 'default'): Database
252    {
253        if ($database === null) {
254            $databases = ['default'];
255        } else if ($database == 'all') {
256            $databases = array_filter(scandir($location . '/database/migrations'), function($value) {
257                return (($value != '.') && ($value != '..'));
258            });
259        } else {
260            $databases = [$database];
261        }
262
263        if (!file_exists($location . '/app/config/database.php')) {
264            $console->write($console->colorize(
265                "The database configuration was not found for '" . $database . "'.", Color::BOLD_RED
266            ));
267        } else {
268            foreach ($databases as $db) {
269                if (!file_exists($location . '/database/seeds/' . $db)) {
270                    $console->write($console->colorize(
271                        "The database seed folder was not found for '" . $db . "'.", Color::BOLD_RED
272                    ));
273                } else {
274                    $dbConfig = include $location . '/app/config/database.php';
275                    if (!isset($dbConfig[$db])) {
276                        $console->write($console->colorize(
277                            "The database configuration was not found for '" . $db . "'.", Color::BOLD_RED
278                        ));
279                    } else {
280                        $dbAdapter = $this->createAdapter($dbConfig[$db]);
281                        $console->write("Running database seeds for '" . $db . "'...");
282
283                        Seeder::run($dbAdapter, $location . '/database/seeds/' . $db);
284
285                        $console->write('Done!');
286                        $console->write();
287                    }
288                }
289            }
290        }
291
292        return $this;
293    }
294
295    /**
296     * Export database
297     *
298     * @param  Console $console
299     * @param  string  $location
300     * @param  string  $database
301     * @return Database
302     */
303    public function export(Console $console, string $location, string $database = 'default'): Database
304    {
305        if ($database === null) {
306            $databases = ['default'];
307        } else if ($database == 'all') {
308            $databases = array_filter(scandir($location . '/database/migrations'), function($value) {
309                return (($value != '.') && ($value != '..'));
310            });
311        } else {
312            $databases = [$database];
313        }
314
315        if (!file_exists($location . '/app/config/database.php')) {
316            $console->write($console->colorize(
317                "The database configuration was not found for '" . $database . "'.", Color::BOLD_RED
318            ));
319        } else {
320            $timestamp = date('YmdHis');
321            $dbConfig  = include $location . '/app/config/database.php';
322            foreach ($databases as $db) {
323                if (!isset($dbConfig[$db])) {
324                    $console->write($console->colorize(
325                        "The database configuration was not found for '" . $db . "'.", Color::BOLD_RED
326                    ));
327                } else if (($dbConfig[$db]['adapter'] != 'mysql') && ($dbConfig[$db]['type'] != 'mysql')) {
328                    $console->write($console->colorize(
329                        "The database is not MySQL. It must be MySQL to perform the export", Color::BOLD_RED
330                    ));
331                } else {
332                    $sqlFile = $dbConfig[$db]['database'] . '-' . $timestamp . '.sql';
333                    $command = 'mysqldump --user=' . $dbConfig[$db]['username'] . ' --password=' .
334                        $dbConfig[$db]['password'] . ' --host=' . $dbConfig[$db]['host'] . ' ' .
335                        $dbConfig[$db]['database'] . ' > ' . $location . '/database/snapshots/' . $db . '/' .$sqlFile;
336
337                    exec($command);
338
339                    $console->write($sqlFile . ' Exported!');
340                    $console->write();
341                }
342            }
343        }
344
345        return $this;
346    }
347
348    /**
349     * Import database
350     *
351     * @param  Console $console
352     * @param  string  $location
353     * @param  string  $importFile
354     * @param  string  $database
355     * @return Database
356     */
357    public function import(Console $console, string $location, string $importFile, string $database = 'default'): Database
358    {
359        if ($database === null) {
360            $databases = ['default'];
361        } else if ($database == 'all') {
362            $databases = array_filter(scandir($location . '/database/migrations'), function($value) {
363                return (($value != '.') && ($value != '..'));
364            });
365        } else {
366            $databases = [$database];
367        }
368
369        if (!file_exists($location . '/app/config/database.php')) {
370            $console->write($console->colorize(
371                "The database configuration was not found for '" . $database . "'.", Color::BOLD_RED
372            ));
373        } else {
374            $sqlImportFile = $location . '/' . $importFile;
375            $dbConfig   = include $location . '/app/config/database.php';
376            foreach ($databases as $db) {
377                if (!isset($dbConfig[$db])) {
378                    $console->write($console->colorize(
379                        "The database configuration was not found for '" . $db . "'.", Color::BOLD_RED
380                    ));
381                } else if (($dbConfig[$db]['adapter'] != 'mysql') && ($dbConfig[$db]['type'] != 'mysql')) {
382                    $console->write($console->colorize(
383                        "The database is not MySQL. It must be MySQL to perform the export", Color::BOLD_RED
384                    ));
385                } else {
386                    $command = 'mysql --user=' . $dbConfig[$db]['username'] . ' --password=' .
387                        $dbConfig[$db]['password'] . ' --host=' . $dbConfig[$db]['host'] . ' ' .
388                        $dbConfig[$db]['database'] . ' < ' . $sqlImportFile;
389
390                    exec($command);
391
392                    $console->write($importFile . ' Imported!');
393                    $console->write();
394                }
395            }
396        }
397
398        return $this;
399    }
400
401    /**
402     * Create database adapter
403     *
404     * @param  array $database
405     * @return Adapter\AbstractAdapter
406     */
407    public function createAdapter(array $database): Adapter\AbstractAdapter
408    {
409        return Db::connect($database['adapter'], array_diff_key($database, array_flip(['adapter'])));
410    }
411
412    /**
413     * Install SQL
414     *
415     * @param  array  $database
416     * @param  string $sqlFile
417     * @return Database
418     */
419    public function install(array $database, string $sqlFile): Database
420    {
421        Db::executeSqlFile($sqlFile, $database['adapter'], array_diff_key($database, array_flip(['adapter'])));
422
423        return $this;
424    }
425
426    /**
427     * Reset database
428     *
429     * @param  Console $console
430     * @param  string  $location
431     * @param  string  $database
432     * @return Database
433     */
434    public function reset(Console $console, string $location, string $database = 'default'): Database
435    {
436        if ($database === null) {
437            $databases = ['default'];
438        } else if ($database == 'all') {
439            $databases = array_filter(scandir($location . '/database/migrations'), function($value) {
440                return (($value != '.') && ($value != '..'));
441            });
442        } else {
443            $databases = [$database];
444        }
445
446        if (!file_exists($location . '/app/config/database.php')) {
447            $console->write($console->colorize(
448                'The database configuration was not found.', Color::BOLD_RED
449            ));
450        } else {
451            foreach ($databases as $db) {
452                if (!file_exists($location . '/database/seeds/' . $db)) {
453                    $console->write($console->colorize(
454                        "The database seed folder was not found for '" . $db . "'.", Color::BOLD_RED
455                    ));
456                } else {
457                    $console->write('Resetting database data...');
458
459                    $dbConfig = include $location . '/app/config/database.php';
460                    if (!isset($dbConfig[$db])) {
461                        $console->write($console->colorize(
462                            "The database configuration was not found for '" . $db . "'.", Color::BOLD_RED
463                        ));
464                    } else {
465                        $dbAdapter = $this->createAdapter($dbConfig[$db]);
466                        $schema    = $dbAdapter->createSchema();
467                        $tables    = $dbAdapter->getTables();
468
469                        if (($dbAdapter instanceof \Pop\Db\Adapter\Mysql) ||
470                            (($dbAdapter instanceof \Pop\Db\Adapter\Pdo) && ($dbAdapter->getType() == 'mysql'))) {
471                            $dbAdapter->query('SET foreign_key_checks = 0');
472                            foreach ($tables as $table) {
473                                $schema->truncate($table);
474                                $dbAdapter->query($schema);
475                            }
476                            $dbAdapter->query('SET foreign_key_checks = 1');
477                        } else if (($dbAdapter instanceof \Pop\Db\Adapter\Pgsql) ||
478                            (($dbAdapter instanceof \Pop\Db\Adapter\Pdo) && ($dbAdapter->getType() == 'pgsql'))) {
479                            foreach ($tables as $table) {
480                                $schema->truncate($table)->cascade();
481                                $dbAdapter->query($schema);
482                            }
483                        } else {
484                            foreach ($tables as $table) {
485                                $schema->truncate($table);
486                                $dbAdapter->query($schema);
487                            }
488                        }
489
490                        $this->seed($console, $location, $db);
491
492                        if (file_exists($location . '/database/migrations/' . $db . '/.current')) {
493                            unlink($location . '/database/migrations/' . $db . '/.current');
494                        }
495                    }
496                }
497            }
498        }
499
500        return $this;
501    }
502
503    /**
504     * Clear database
505     *
506     * @param  Console $console
507     * @param  string  $location
508     * @param  string  $database
509     * @return Database
510     */
511    public function clear(Console $console, string $location, string $database = 'default'): Database
512    {
513        if ($database === null) {
514            $databases = ['default'];
515        } else if ($database == 'all') {
516            $databases = array_filter(scandir($location . '/database/migrations'), function($value) {
517                return (($value != '.') && ($value != '..'));
518            });
519        } else {
520            $databases = [$database];
521        }
522
523        if (!file_exists($location . '/app/config/database.php')) {
524            $console->write($console->colorize(
525                'The database configuration was not found.', Color::BOLD_RED
526            ));
527        } else {
528            foreach ($databases as $db) {
529                $console->write('Clearing database data...');
530
531                $dbConfig = include $location . '/app/config/database.php';
532                if (!isset($dbConfig[$db])) {
533                    $console->write($console->colorize(
534                        "The database configuration was not found for '" . $db . "'.", Color::BOLD_RED
535                    ));
536                } else {
537                    $dbAdapter = $this->createAdapter($dbConfig[$db]);
538                    $schema    = $dbAdapter->createSchema();
539                    $tables    = $dbAdapter->getTables();
540
541                    if (($dbAdapter instanceof \Pop\Db\Adapter\Mysql) ||
542                        (($dbAdapter instanceof \Pop\Db\Adapter\Pdo) && ($dbAdapter->getType() == 'mysql'))) {
543                        $dbAdapter->query('SET foreign_key_checks = 0');
544                        foreach ($tables as $table) {
545                            $schema->drop($table);
546                            $dbAdapter->query($schema);
547                        }
548                        $dbAdapter->query('SET foreign_key_checks = 1');
549                    } else if (($dbAdapter instanceof \Pop\Db\Adapter\Pgsql) ||
550                        (($dbAdapter instanceof \Pop\Db\Adapter\Pdo) && ($dbAdapter->getType() == 'pgsql'))) {
551                        foreach ($tables as $table) {
552                            $schema->drop($table)->cascade();
553                            $dbAdapter->query($schema);
554                        }
555                    } else {
556                        foreach ($tables as $table) {
557                            $schema->drop($table);
558                            $dbAdapter->query($schema);
559                        }
560                    }
561
562                    if (file_exists($location . '/database/migrations/' . $db . '/.current')) {
563                        unlink($location . '/database/migrations/' . $db . '/.current');
564                    }
565
566                    $console->write();
567                    $console->write('Done!');
568                }
569            }
570        }
571
572        return $this;
573    }
574
575}