Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
96.90% covered (success)
96.90%
313 / 323
81.82% covered (success)
81.82%
9 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
Application
96.90% covered (success)
96.90%
313 / 323
81.82% covered (success)
81.82%
9 / 11
80
0.00% covered (danger)
0.00%
0 / 1
 init
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 parseNamespace
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
1 / 1
5
 splitIntoWords
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 install
100.00% covered (success)
100.00%
117 / 117
100.00% covered (success)
100.00%
1 / 1
22
 installFrontend
86.96% covered (success)
86.96%
20 / 23
0.00% covered (danger)
0.00%
0 / 1
7.11
 createCommand
100.00% covered (success)
100.00%
29 / 29
100.00% covered (success)
100.00%
1 / 1
6
 createController
100.00% covered (success)
100.00%
43 / 43
100.00% covered (success)
100.00%
1 / 1
11
 createModel
85.42% covered (success)
85.42%
41 / 48
0.00% covered (danger)
0.00%
0 / 1
12.45
 createView
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
5
 getNamespace
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
7
 resolveAppInstance
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
3
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\Code\Generator;
18use Pop\Dir\Dir;
19use Pop\Utils\AbstractModel;
20use Pop\Kettle\Exception;
21use Pop\Utils\Str;
22
23/**
24 * Application model class
25 *
26 * @category   Pop\Kettle
27 * @package    Pop\Kettle
28 * @author     Nick Sagona, III <nick@popphp.org>
29 * @copyright  Copyright (c) 2009-2026 Nick Sagona, III
30 * @license    https://www.popphp.org/license     New BSD License
31 * @version    3.0.0
32 */
33class Application extends AbstractModel
34{
35
36    /**
37     * Init application
38     *
39     * @param  string  $location
40     * @param  string  $namespace
41     * @param  bool    $cliOnly
42     * @param  string  $name
43     * @param  string  $url
44     * @param  bool    $cliApp
45     * @param  bool    $createDb
46     * @param  ?string $frontend
47     * @return void
48     */
49    public function init(
50        string $location, string $namespace, bool $cliOnly = false,
51        string $name = 'App', string $url = '', bool $cliApp = false, bool $createDb = false,
52        ?string $frontend = null
53    ): void
54    {
55        $this->install($cliOnly, $location, $namespace, $name, $url, $cliApp, $createDb, $frontend);
56    }
57
58    /**
59     * Parse a raw namespace value typed by the user into a valid PHP namespace,
60     * a filesystem-safe kebab-case slug and a human-readable full name.
61     *
62     * @param  string $input
63     * @throws Exception
64     * @return array{namespace: string, slug: string, fullName: string}
65     */
66    public static function parseNamespace(string $input): array
67    {
68        $segments       = preg_split('/[\\\\\/\s]+/', trim($input), -1, PREG_SPLIT_NO_EMPTY);
69        $namespaceParts = [];
70        $allWords       = [];
71
72        foreach ($segments as $segment) {
73            $words = self::splitIntoWords($segment);
74            if (empty($words)) {
75                continue;
76            }
77
78            $part = implode('', array_map(fn($word) => ucfirst(strtolower($word)), $words));
79            if (preg_match('/^[0-9]/', $part)) {
80                $part = '_' . $part;
81            }
82
83            $namespaceParts[] = $part;
84            array_push($allWords, ...$words);
85        }
86
87        if (empty($namespaceParts)) {
88            throw new Exception("Error: Unable to derive a valid namespace from '" . $input . "'.");
89        }
90
91        return [
92            'namespace' => implode('\\', $namespaceParts),
93            'slug'      => strtolower(implode('-', $allWords)),
94            'fullName'  => implode(' ', array_map(fn($word) => ucfirst(strtolower($word)), $allWords)),
95        ];
96    }
97
98    /**
99     * Split a single namespace segment into its component words, on
100     * non-alphanumeric separators and camelCase boundaries.
101     *
102     * @param  string $segment
103     * @return array
104     */
105    protected static function splitIntoWords(string $segment): array
106    {
107        $segment = preg_replace('/[^A-Za-z0-9]+/', ' ', $segment);
108        $segment = preg_replace('/([a-z0-9])([A-Z])/', '$1 $2', $segment);
109        $segment = preg_replace('/([A-Z]+)([A-Z][a-z])/', '$1 $2', $segment);
110
111        return preg_split('/\s+/', trim($segment), -1, PREG_SPLIT_NO_EMPTY);
112    }
113
114    /**
115     * Install application files
116     *
117     * @param  bool    $cliOnly
118     * @param  string  $location
119     * @param  string  $namespace
120     * @param  string  $name
121     * @param  string  $url
122     * @param  bool    $cliApp
123     * @param  bool    $createDb
124     * @param  ?string $frontend
125     * @return void
126     */
127    public function install(
128        bool $cliOnly, string $location, string $namespace, string $name = 'App',
129        string $url = '', bool $cliApp = false, bool $createDb = false,
130        ?string $frontend = null
131    ): void
132    {
133        $parsed    = self::parseNamespace($namespace);
134        $namespace = $parsed['namespace'];
135        $script    = $parsed['slug'];
136        $fullName  = $parsed['fullName'];
137
138        $path   = realpath(__DIR__ . '/../../config/templates/codebase/' . ($cliOnly ? 'cli' : 'full'));
139        $dir    = new Dir($path);
140        foreach ($dir as $entry) {
141            if (is_dir($path . DIRECTORY_SEPARATOR . $entry)) {
142                $d = new Dir($path . DIRECTORY_SEPARATOR . $entry);
143                $d->copyTo($location);
144            }
145        }
146
147        $dir = new Dir($location . '/app', [
148            'filesOnly' => true,
149            'recursive' => true,
150            'absolute'  => true
151        ]);
152
153        foreach ($dir as $file) {
154            file_put_contents($file, str_replace(['MyApp', 'myapp'], [$namespace, $script], file_get_contents($file)));
155        }
156
157        // The main Application class' FULL_NAME const should be the human-readable
158        // name, not the raw PHP namespace token used everywhere else in the file
159        $appClassFile = $location . DIRECTORY_SEPARATOR . 'app/src/Application.php';
160        if (file_exists($appClassFile)) {
161            file_put_contents($appClassFile, str_replace(
162                "FULL_NAME = '" . $namespace . "'",
163                "FULL_NAME = '" . $fullName . "'",
164                file_get_contents($appClassFile)
165            ));
166        }
167
168        // Set up web /public folder and files
169        if (!$cliOnly) {
170            mkdir($location . DIRECTORY_SEPARATOR . 'public');
171            copy(__DIR__ . '/../../config/templates/public/.htaccess', $location . DIRECTORY_SEPARATOR . 'public/.htaccess');
172            copy(__DIR__ . '/../../config/templates/public/index.php', $location . DIRECTORY_SEPARATOR . 'public/index.php');
173
174            file_put_contents(
175                $location . DIRECTORY_SEPARATOR . 'public' . DIRECTORY_SEPARATOR . 'index.php',
176                str_replace(
177                    ['MyApp', 'myapp'], [$namespace, $script],
178                    file_get_contents($location . DIRECTORY_SEPARATOR . 'public' . DIRECTORY_SEPARATOR . 'index.php')
179                )
180            );
181
182            // Copy view files
183            mkdir($location . DIRECTORY_SEPARATOR . 'app' . DIRECTORY_SEPARATOR . 'view');
184            $indexView = ($frontend !== null) ? 'index-' . $frontend . '.phtml' : 'index.phtml';
185            copy(__DIR__ . '/../../config/templates/view/' . $indexView, $location . DIRECTORY_SEPARATOR . 'app/view/index.phtml');
186            copy(__DIR__ . '/../../config/templates/view/error.phtml', $location . DIRECTORY_SEPARATOR . 'app/view/error.phtml');
187            copy(__DIR__ . '/../../config/templates/view/exception.phtml', $location . DIRECTORY_SEPARATOR . 'app/view/exception.phtml');
188            copy(__DIR__ . '/../../config/templates/view/maintenance.phtml', $location . DIRECTORY_SEPARATOR . 'app/view/maintenance.phtml');
189
190            if ($frontend !== null) {
191                $this->installFrontend($frontend, $location, $namespace, $script);
192            }
193        }
194
195        // Set up CLI /script folder and application script
196        if ($cliApp) {
197            mkdir($location . DIRECTORY_SEPARATOR . 'script');
198            copy(__DIR__ . '/../../config/templates/script/app', $location . DIRECTORY_SEPARATOR . 'script/app');
199
200            file_put_contents(
201                $location . DIRECTORY_SEPARATOR . 'script' . DIRECTORY_SEPARATOR . 'app',
202                str_replace(
203                    ['MyApp', 'myapp'], [$namespace, $script],
204                    file_get_contents($location . DIRECTORY_SEPARATOR . 'script' . DIRECTORY_SEPARATOR . 'app')
205                )
206            );
207
208            rename(
209                $location . DIRECTORY_SEPARATOR . 'script' . DIRECTORY_SEPARATOR . 'app',
210                $location . DIRECTORY_SEPARATOR . 'script' . DIRECTORY_SEPARATOR . $script
211            );
212            chmod($location . DIRECTORY_SEPARATOR . 'script' . DIRECTORY_SEPARATOR . $script, 0755);
213        } else if (file_exists($location . DIRECTORY_SEPARATOR . 'app' . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'Console' . DIRECTORY_SEPARATOR . 'Controller')) {
214            $dir = new Dir($location . DIRECTORY_SEPARATOR . 'app' . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'Console' . DIRECTORY_SEPARATOR . 'Controller');
215            $dir->emptyDir(true);
216        }
217
218        // Add writable /data folder
219        mkdir($location . DIRECTORY_SEPARATOR . 'data');
220        chmod($location . DIRECTORY_SEPARATOR . 'data', 0777);
221        touch($location . DIRECTORY_SEPARATOR . 'data' . DIRECTORY_SEPARATOR . '.empty');
222
223        // Set up database folder
224        if ($createDb) {
225            mkdir($location . DIRECTORY_SEPARATOR . 'database');
226            $dbPath = realpath(__DIR__ . '/../../config/templates/database');
227            $dir    = new Dir($dbPath);
228            foreach ($dir as $entry) {
229                if (is_dir($dbPath . DIRECTORY_SEPARATOR . $entry)) {
230                    $d = new Dir($dbPath . DIRECTORY_SEPARATOR . $entry);
231                    $d->copyTo($location . DIRECTORY_SEPARATOR . 'database');
232                }
233            }
234
235            copy(
236                __DIR__ . '/../../config/templates/database.php',
237                $location . DIRECTORY_SEPARATOR . 'app' . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'database.php'
238            );
239        } else {
240            if (file_exists($location . DIRECTORY_SEPARATOR . 'app' . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'app.http.php')) {
241                file_put_contents(
242                    $location . DIRECTORY_SEPARATOR . 'app' . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'app.http.php',
243                    str_replace(
244                        "    'database' => include __DIR__ . '/database.php',", "",
245                        file_get_contents($location . DIRECTORY_SEPARATOR . 'app' . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'app.http.php')
246                    )
247                );
248            }
249            if (file_exists($location . DIRECTORY_SEPARATOR . 'app' . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'app.console.php')) {
250                file_put_contents(
251                    $location . DIRECTORY_SEPARATOR . 'app' . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'app.console.php',
252                    str_replace(
253                        "    'database' => include __DIR__ . '/database.php',", "",
254                        file_get_contents($location . DIRECTORY_SEPARATOR . 'app' . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'app.console.php')
255                    )
256                );
257            }
258        }
259
260        // Register the app namespace in composer.json's PSR-4 autoload map
261        $composerFile = $location . DIRECTORY_SEPARATOR . 'composer.json';
262        if (file_exists($composerFile)) {
263            $composer = json_decode(file_get_contents($composerFile), true);
264            $psr4Key  = $namespace . '\\';
265
266            if (!isset($composer['autoload']['psr-4'][$psr4Key])) {
267                $composer['autoload']['psr-4'][$psr4Key] = 'app/src/';
268                file_put_contents(
269                    $composerFile,
270                    json_encode($composer, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . PHP_EOL
271                );
272            }
273        }
274
275        // Copy .env file over and populate with values
276        if (!file_exists($location . DIRECTORY_SEPARATOR . '/.env')) {
277            copy(
278                __DIR__ . '/../../config/templates/.env.example',
279                $location . DIRECTORY_SEPARATOR . '/.env'
280            );
281        }
282
283        if (str_contains($name, ' ') && !str_starts_with($name, '"') && !str_ends_with($name, '"')) {
284            $name = '"' . $name . '"';
285        }
286
287        $contents = str_replace([
288            'APP_NAME=MyApp',
289            'APP_URL=http://localhost',
290        ], [
291            'APP_NAME=' . $name,
292            'APP_URL=' . $url,
293        ], file_get_contents($location . DIRECTORY_SEPARATOR . '/.env'));
294
295        file_put_contents($location . DIRECTORY_SEPARATOR . '/.env', $contents);
296    }
297
298    /**
299     * Install front-end tooling (package.json, vite config, source assets)
300     *
301     * @param  string $frontend
302     * @param  string $location
303     * @param  string $namespace
304     * @param  string $script
305     * @return void
306     */
307    protected function installFrontend(string $frontend, string $location, string $namespace, string $script): void
308    {
309        $frontendPath = realpath(__DIR__ . '/../../config/templates/frontend/' . $frontend);
310        $dir          = new Dir($frontendPath);
311
312        foreach ($dir as $entry) {
313            $entryPath = $frontendPath . DIRECTORY_SEPARATOR . $entry;
314            if (is_dir($entryPath)) {
315                if (!file_exists($location . DIRECTORY_SEPARATOR . $entry)) {
316                    mkdir($location . DIRECTORY_SEPARATOR . $entry);
317                }
318                (new Dir($entryPath))->copyTo($location . DIRECTORY_SEPARATOR . $entry, false);
319            } else {
320                copy($entryPath, $location . DIRECTORY_SEPARATOR . $entry);
321            }
322        }
323
324        $placeholders = ['MyApp', 'myapp'];
325        $replacements = [$namespace, $script];
326
327        $packageJson = $location . DIRECTORY_SEPARATOR . 'package.json';
328        file_put_contents($packageJson, str_replace($placeholders, $replacements, file_get_contents($packageJson)));
329
330        $assetsDir = new Dir($location . DIRECTORY_SEPARATOR . 'app' . DIRECTORY_SEPARATOR . 'assets', [
331            'filesOnly' => true, 'recursive' => true, 'absolute' => true
332        ]);
333        foreach ($assetsDir as $file) {
334            file_put_contents($file, str_replace($placeholders, $replacements, file_get_contents($file)));
335        }
336
337        $gitignore = $location . DIRECTORY_SEPARATOR . '.gitignore';
338        if (!file_exists($gitignore)) {
339            file_put_contents($gitignore, 'node_modules/' . PHP_EOL);
340        } else if (!str_contains(file_get_contents($gitignore), 'node_modules/')) {
341            file_put_contents($gitignore, 'node_modules/' . PHP_EOL, FILE_APPEND);
342        }
343    }
344
345    /**
346     * Create command method
347     *
348     * @param  string $command
349     * @param  string $location
350     * @param  bool   $app
351     * @throws Exception
352     * @return void
353     */
354    public function createCommand(string $command, string $location, bool $app = false): void
355    {
356        $command   = strtolower($command);
357        $namespace = $this->getNamespace($location);
358
359        $commandFolder = $location . DIRECTORY_SEPARATOR . 'app' . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR .
360            'Console' . DIRECTORY_SEPARATOR . 'Command';
361
362        if (!$app) {
363            $commandFolder .= DIRECTORY_SEPARATOR . 'Kettle';
364        }
365
366        if (!file_exists($commandFolder)) {
367            throw new Exception('Error: The command folder and namespace has not been created');
368        }
369
370        $classCommandName = (str_contains($command, ':')) ?
371            substr($command, (strrpos($command, ':') + 1)) : $command;
372
373        if (!file_exists($commandFolder . DIRECTORY_SEPARATOR . $classCommandName . '.php')) {
374            $classCommandName = ucfirst(Str::convertToCamelCase($classCommandName, '-'));
375
376            $commandNamespace = $namespace . "\\Console\\Command";
377
378            if (!$app) {
379                $commandNamespace .= '\\Kettle';
380            }
381
382            $namespaceObject  = new Generator\NamespaceGenerator($commandNamespace);
383
384            $commandClassObject = new Generator\ClassGenerator($classCommandName);
385            $commandClassObject->setParent("\\Pop\\Console\\Command\\AbstractCommand");
386
387            $nameProperty   = new Generator\PropertyGenerator('name', '?string', $command);
388            $paramsProperty = new Generator\PropertyGenerator('params', '?string');
389            $helpProperty   = new Generator\PropertyGenerator('help', '?string', "This is the " . $command . " command");
390            $handleMethod   = new Generator\MethodGenerator('handle', 'public');
391            $handleMethod->setBody('/** Add command code here. */')
392                ->setDocblock(new Generator\DocblockGenerator("  \$this->application and \$this->console are both available."));
393
394            $commandClassObject->addProperties([$nameProperty, $paramsProperty, $helpProperty])
395                ->addMethod($handleMethod);
396
397            $code = new Generator();
398            $code->addCodeObjects([$namespaceObject, $commandClassObject]);
399            $code->writeToFile($commandFolder . DIRECTORY_SEPARATOR . $classCommandName . '.php');
400        }
401    }
402
403    /**
404     * Create controller method
405     *
406     * @param  string $ctrl
407     * @param  string $location
408     * @param  ?bool  $cli
409     * @throws Exception
410     * @return string
411     */
412    public function createController(string $ctrl, string $location, ?bool $cli = null): string
413    {
414        $namespace = $this->getNamespace($location);
415
416        $cliFolder = $location . DIRECTORY_SEPARATOR . 'app' . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR .
417            'Console' . DIRECTORY_SEPARATOR . 'Controller';
418
419        $httpFolder = $location . DIRECTORY_SEPARATOR . 'app' . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR .
420            'Http' . DIRECTORY_SEPARATOR . 'Controller';
421
422        // Create CLI controller
423        if ($cli === true) {
424            $scriptFolder = $location . DIRECTORY_SEPARATOR . 'script';
425
426            if (!file_exists($cliFolder) || !file_exists($scriptFolder)) {
427                throw new Exception('Error: This application was not initialized with a stand-alone console application.');
428            }
429            $cliNamespace = $namespace . "\\Console\\Controller";
430
431            if (strpos($ctrl, DIRECTORY_SEPARATOR)) {
432                $folders = explode(DIRECTORY_SEPARATOR, $ctrl);
433                $ctrl    = array_pop($folders);
434
435                foreach ($folders as $folder) {
436                    $cliNamespace .= "\\" . $folder;
437                    $cliFolder    .= DIRECTORY_SEPARATOR . $folder;
438                    if (!file_exists($cliFolder)) {
439                        mkdir($cliFolder);
440                    }
441                }
442            }
443            $cliCtrlClassObject = new Generator\ClassGenerator($ctrl);
444            $cliCtrlClassObject->setParent("\\" . $namespace . "\\Console\\Controller\\AbstractController");
445
446            $namespaceObject = new Generator\NamespaceGenerator($cliNamespace);
447
448            $code = new Generator();
449            $code->addCodeObjects([$namespaceObject, $cliCtrlClassObject]);
450            $code->writeToFile($cliFolder . DIRECTORY_SEPARATOR . $ctrl . '.php');
451
452            return $cliNamespace . "\\" . $ctrl;
453        } else {
454            // Create HTTP controller
455            if (!file_exists($httpFolder)) {
456                throw new Exception('Error: The HTTP folder and namespace has not been created');
457            }
458            $httpNamespace = $namespace . "\\Http\\Controller";
459            if (strpos($ctrl, DIRECTORY_SEPARATOR)) {
460                $folders = explode(DIRECTORY_SEPARATOR, $ctrl);
461                $ctrl    = array_pop($folders);
462
463                foreach ($folders as $folder) {
464                    $httpNamespace .= "\\" . $folder;
465                    $httpFolder    .= DIRECTORY_SEPARATOR . $folder;
466                    if (!file_exists($httpFolder)) {
467                        mkdir($httpFolder);
468                    }
469                }
470            }
471            $httpCtrlClassObject = new Generator\ClassGenerator($ctrl);
472            $httpCtrlClassObject->setParent("\\" . $namespace . "\\Http\\Controller\\AbstractController");
473
474            $namespaceObject = new Generator\NamespaceGenerator($httpNamespace);
475
476            $code = new Generator();
477            $code->addCodeObjects([$namespaceObject, $httpCtrlClassObject]);
478            $code->writeToFile($httpFolder . DIRECTORY_SEPARATOR . $ctrl . '.php');
479
480            return $httpNamespace . "\\" . $ctrl;
481        }
482    }
483
484    /**
485     * Create model method
486     *
487     * @param  string $model
488     * @param  string $location
489     * @param  bool   $data
490     * @return string
491     */
492    public function createModel(string $model, string $location, bool $data = false): string
493    {
494        $namespace   = $this->getNamespace($location) . "\\Model";
495        $modelFolder = $location . DIRECTORY_SEPARATOR . 'app' . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'Model';
496        if (!file_exists($modelFolder)) {
497            mkdir($modelFolder);
498        }
499
500        if (strpos($model, DIRECTORY_SEPARATOR)) {
501            $folders = explode(DIRECTORY_SEPARATOR, $model);
502            $model   = array_pop($folders);
503
504            foreach ($folders as $folder) {
505                $namespace   .= "\\" . $folder;
506                $modelFolder .= DIRECTORY_SEPARATOR . $folder;
507                if (!file_exists($modelFolder)) {
508                    mkdir($modelFolder);
509                }
510            }
511        }
512
513        if ($data) {
514            $abstractModel = 'AbstractDataModel';
515            $useNamespace  = 'Pop\Db\Model\AbstractDataModel';
516
517        } else {
518            $abstractModel = 'AbstractModel';
519            $useNamespace  = 'Pop\Utils\AbstractModel';
520        }
521
522        $modelClassObject = new Generator\ClassGenerator($model);
523        $modelClassObject->setParent($abstractModel);
524
525        $namespaceObject = new Generator\NamespaceGenerator($namespace);
526        $namespaceObject->addUse($useNamespace);
527
528        $code = new Generator();
529        $code->addCodeObjects([$namespaceObject, $modelClassObject]);
530        $code->writeToFile($modelFolder . DIRECTORY_SEPARATOR . $model . '.php');
531
532        if ($data) {
533            $table = $model;
534
535            if (!str_ends_with($model, 's')) {
536                $table .= 's';
537            }
538
539            $tableNamespace = $this->getNamespace($location) . "\\Table";
540            $tableFolder    = $location . DIRECTORY_SEPARATOR . 'app' . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'Table';
541            if (!file_exists($tableFolder)) {
542                mkdir($tableFolder);
543            }
544
545            if (strpos($table, DIRECTORY_SEPARATOR)) {
546                $folders = explode(DIRECTORY_SEPARATOR, $table);
547                $table   = array_pop($folders);
548
549                foreach ($folders as $folder) {
550                    $namespace   .= "\\" . $folder;
551                    $tableFolder .= DIRECTORY_SEPARATOR . $folder;
552                    if (!file_exists($tableFolder)) {
553                        mkdir($tableFolder);
554                    }
555                }
556            }
557
558            $tableClassObject = new Generator\ClassGenerator($table);
559            $tableClassObject->setParent('Record');
560
561            $tableNamespaceObject = new Generator\NamespaceGenerator($tableNamespace);
562            $tableNamespaceObject->addUse('Pop\Db\Record');
563
564            $code = new Generator();
565            $code->addCodeObjects([$tableNamespaceObject, $tableClassObject]);
566            $code->writeToFile($tableFolder . DIRECTORY_SEPARATOR . $table . '.php');
567        }
568
569        return $namespace . "\\" . $model;
570    }
571
572    /**
573     * Create view method
574     *
575     * @param  string $view
576     * @param  string $location
577     * @return string
578     */
579    public function createView(string $view, string $location): string
580    {
581        $origView   = $view;
582        $viewFolder = $location . DIRECTORY_SEPARATOR . 'app' . DIRECTORY_SEPARATOR . 'view';
583        if (!file_exists($viewFolder)) {
584            mkdir($viewFolder);
585        }
586
587        if (strpos($view, DIRECTORY_SEPARATOR)) {
588            $folders = explode(DIRECTORY_SEPARATOR, $view);
589            $view    = array_pop($folders);
590
591            foreach ($folders as $folder) {
592                $viewFolder .= DIRECTORY_SEPARATOR . $folder;
593                if (!file_exists($viewFolder)) {
594                    mkdir($viewFolder);
595                }
596            }
597        }
598
599        touch($viewFolder . DIRECTORY_SEPARATOR . $view);
600        $index = file_get_contents(realpath(__DIR__ . '/../../config/templates/view/index.phtml'));
601        file_put_contents($viewFolder . DIRECTORY_SEPARATOR . $view, $index);
602
603        return $origView;
604    }
605
606    /**
607     * Get namespace
608     *
609     * @param  string $location
610     * @throws Exception
611     * @return string
612     */
613    public function getNamespace(string $location): string
614    {
615        if (file_exists($location . DIRECTORY_SEPARATOR . 'app') &&
616            file_exists($location . DIRECTORY_SEPARATOR . 'app' . DIRECTORY_SEPARATOR . 'src') &&
617            file_exists($location . DIRECTORY_SEPARATOR . 'app' . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'Application.php')) {
618            $fileContents = file_get_contents($location . DIRECTORY_SEPARATOR . 'app' . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'Application.php');
619            $namespace    = substr($fileContents, (strpos($fileContents, 'namespace ') + 10));
620            $namespace    = substr($namespace, 0, strpos($namespace, ';'));
621
622            return $namespace;
623        } else if (file_exists($location . DIRECTORY_SEPARATOR . 'app') &&
624            file_exists($location . DIRECTORY_SEPARATOR . 'app' . DIRECTORY_SEPARATOR . 'src') &&
625            file_exists($location . DIRECTORY_SEPARATOR . 'app' . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'Module.php')) {
626            $fileContents = file_get_contents($location . DIRECTORY_SEPARATOR . 'app' . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'Module.php');
627            $namespace    = substr($fileContents, (strpos($fileContents, 'namespace ') + 10));
628            $namespace    = substr($namespace, 0, strpos($namespace, ';'));
629
630            return $namespace;
631        } else {
632            throw new Exception('Error: Unable to detect namespace.');
633        }
634    }
635
636    /**
637     * Resolve a full instance of the consuming application, if one is scaffolded, merging
638     * $baseRoutes with its own app/src/Console/Command routes
639     *
640     * @param  string $dir
641     * @param  mixed  $autoloader
642     * @param  array  $baseRoutes
643     * @throws Exception
644     * @return ?\Pop\Application
645     */
646    public function resolveAppInstance(string $dir, mixed $autoloader, array $baseRoutes): ?\Pop\Application
647    {
648        $namespace = $this->getNamespace($dir);
649        $appClass  = $namespace . '\Application';
650
651        if (!class_exists($appClass)) {
652            return null;
653        }
654
655        $config = file_exists($dir . '/app/config/app.console.php')
656            ? include $dir . '/app/config/app.console.php'
657            : [];
658
659        $config['routes'] = \Pop\Console\CommandRegistry::loadRoutes($baseRoutes, $dir . '/app/src/Console/Command');
660
661        return new $appClass($autoloader, $config);
662    }
663
664}