Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
91.30% covered (success)
91.30%
294 / 322
66.67% covered (warning)
66.67%
6 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
Database
91.30% covered (success)
91.30%
294 / 322
66.67% covered (warning)
66.67%
6 / 9
107.71
0.00% covered (danger)
0.00%
0 / 1
 configure
93.20% covered (success)
93.20%
137 / 147
0.00% covered (danger)
0.00%
0 / 1
38.45
 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
83.67% covered (success)
83.67%
41 / 49
0.00% covered (danger)
0.00%
0 / 1
22.92
 clear
76.19% covered (success)
76.19%
32 / 42
0.00% covered (danger)
0.00%
0 / 1
19.46
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\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 <nick@popphp.org>
30 * @copyright  Copyright (c) 2009-2026 Nick Sagona, III
31 * @license    https://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/.env.example',
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                            // The tables are truncated one statement at a time, so the schema object has
485                            // to be cleared between them - rendering it does not clear it.
486                            foreach ($tables as $table) {
487                                $schema->truncate($table);
488                                $dbAdapter->query($schema);
489                                $schema->reset();
490                            }
491                            $dbAdapter->query('SET foreign_key_checks = 1');
492                        } else if (($dbAdapter instanceof \Pop\Db\Adapter\Pgsql) ||
493                            (($dbAdapter instanceof \Pop\Db\Adapter\Pdo) && ($dbAdapter->getType() == 'pgsql'))) {
494                            // The tables are truncated one statement at a time, so the schema object has
495                            // to be cleared between them - rendering it does not clear it.
496                            foreach ($tables as $table) {
497                                $schema->truncate($table)->cascade();
498                                $dbAdapter->query($schema);
499                                $schema->reset();
500                            }
501                        } else if (($dbAdapter instanceof \Pop\Db\Adapter\Sqlite) ||
502                            (($dbAdapter instanceof \Pop\Db\Adapter\Pdo) && ($dbAdapter->getType() == 'sqlite'))) {
503                            // SQLite has no TRUNCATE statement, so delete all rows instead
504                            foreach ($tables as $table) {
505                                $dbAdapter->query('DELETE FROM ' . $schema->quoteId($table));
506                            }
507                        } else {
508                            // The tables are truncated one statement at a time, so the schema object has
509                            // to be cleared between them - rendering it does not clear it.
510                            foreach ($tables as $table) {
511                                $schema->truncate($table);
512                                $dbAdapter->query($schema);
513                                $schema->reset();
514                            }
515                        }
516
517                        $this->seed($console, $location, $db);
518
519                        if (file_exists($location . '/database/migrations/' . $db . '/.current')) {
520                            unlink($location . '/database/migrations/' . $db . '/.current');
521                        }
522                    }
523                }
524            }
525        }
526
527        return $this;
528    }
529
530    /**
531     * Clear database
532     *
533     * @param  Console $console
534     * @param  string  $location
535     * @param  string  $database
536     * @return Database
537     */
538    public function clear(Console $console, string $location, string $database = 'default'): Database
539    {
540        if ($database == 'all') {
541            $databases = array_filter(scandir($location . '/database/migrations'), function($value) {
542                return (($value != '.') && ($value != '..'));
543            });
544        } else {
545            $databases = [$database];
546        }
547
548        if (!file_exists($location . '/app/config/database.php')) {
549            $console->write($console->colorize(
550                'The database configuration was not found.', Color::BOLD_RED
551            ));
552        } else {
553            foreach ($databases as $db) {
554                $console->write('Clearing database data...');
555
556                $dbConfig = include $location . '/app/config/database.php';
557                if (!isset($dbConfig[$db])) {
558                    $console->write($console->colorize(
559                        "The database configuration was not found for '" . $db . "'.", Color::BOLD_RED
560                    ));
561                } else {
562                    $dbAdapter = $this->createAdapter($dbConfig[$db]);
563                    $schema    = $dbAdapter->createSchema();
564                    $tables    = $dbAdapter->getTables();
565
566                    if (($dbAdapter instanceof \Pop\Db\Adapter\Mysql) ||
567                        (($dbAdapter instanceof \Pop\Db\Adapter\Pdo) && ($dbAdapter->getType() == 'mysql'))) {
568                        $dbAdapter->query('SET foreign_key_checks = 0');
569                        // The tables are dropped one statement at a time, so the schema object has to be
570                        // cleared between them - rendering it does not clear it.
571                        foreach ($tables as $table) {
572                            $schema->drop($table);
573                            $dbAdapter->query($schema);
574                            $schema->reset();
575                        }
576                        $dbAdapter->query('SET foreign_key_checks = 1');
577                    } else if (($dbAdapter instanceof \Pop\Db\Adapter\Pgsql) ||
578                        (($dbAdapter instanceof \Pop\Db\Adapter\Pdo) && ($dbAdapter->getType() == 'pgsql'))) {
579                        // The tables are dropped one statement at a time, so the schema object has to be
580                        // cleared between them - rendering it does not clear it.
581                        foreach ($tables as $table) {
582                            $schema->drop($table)->cascade();
583                            $dbAdapter->query($schema);
584                            $schema->reset();
585                        }
586                    } else {
587                        // The tables are dropped one statement at a time, so the schema object has to be
588                        // cleared between them - rendering it does not clear it.
589                        foreach ($tables as $table) {
590                            $schema->drop($table);
591                            $dbAdapter->query($schema);
592                            $schema->reset();
593                        }
594                    }
595
596                    if (file_exists($location . '/database/migrations/' . $db . '/.current')) {
597                        unlink($location . '/database/migrations/' . $db . '/.current');
598                    }
599
600                    $console->write();
601                    $console->write('Done!');
602                }
603            }
604        }
605
606        return $this;
607    }
608
609}