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