From 4d3f5e088c11b4b8c71367bb4fa49d96b886f349 Mon Sep 17 00:00:00 2001 From: Ash Monsh Date: Sat, 27 Jan 2024 13:55:11 +0300 Subject: [PATCH] =?UTF-8?q?=F0=9F=95=B9=EF=B8=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/NewCommand.php | 382 ++++++++++++++++--- src/NewCommandFilament.php | 749 +++++++++++++++++++++++++++++++++++++ 2 files changed, 1077 insertions(+), 54 deletions(-) create mode 100644 src/NewCommandFilament.php diff --git a/src/NewCommand.php b/src/NewCommand.php index 3603360..6f661c8 100644 --- a/src/NewCommand.php +++ b/src/NewCommand.php @@ -17,8 +17,8 @@ use function Laravel\Prompts\confirm; use function Laravel\Prompts\multiselect; use function Laravel\Prompts\select; -use function Laravel\Prompts\text; use function Laravel\Prompts\spin; +use function Laravel\Prompts\text; class NewCommand extends Command { @@ -40,16 +40,26 @@ protected function configure() { $this ->setName('new') - ->setDescription('Create a new Filament application') + ->setDescription('Create a new Laravel application') ->addArgument('name', InputArgument::REQUIRED) - ->addArgument('panel', InputArgument::REQUIRED) ->addOption('dev', null, InputOption::VALUE_NONE, 'Installs the latest "development" release') ->addOption('git', null, InputOption::VALUE_NONE, 'Initialize a Git repository') ->addOption('branch', null, InputOption::VALUE_REQUIRED, 'The branch that should be created for a new repository', $this->defaultBranch()) ->addOption('github', null, InputOption::VALUE_OPTIONAL, 'Create a new repository on GitHub', false) ->addOption('organization', null, InputOption::VALUE_REQUIRED, 'The GitHub organization to create the new repository for') + ->addOption('stack', null, InputOption::VALUE_OPTIONAL, 'The Breeze / Jetstream stack that should be installed') + ->addOption('breeze', null, InputOption::VALUE_NONE, 'Installs the Laravel Breeze scaffolding') + ->addOption('jet', null, InputOption::VALUE_NONE, 'Installs the Laravel Jetstream scaffolding') + ->addOption('dark', null, InputOption::VALUE_NONE, 'Indicate whether Breeze or Jetstream should be scaffolded with dark mode support') + ->addOption('typescript', null, InputOption::VALUE_NONE, 'Indicate whether Breeze should be scaffolded with TypeScript support (Experimental)') + ->addOption('ssr', null, InputOption::VALUE_NONE, 'Indicate whether Breeze or Jetstream should be scaffolded with Inertia SSR support') + ->addOption('api', null, InputOption::VALUE_NONE, 'Indicates whether Jetstream should be scaffolded with API support') + ->addOption('teams', null, InputOption::VALUE_NONE, 'Indicates whether Jetstream should be scaffolded with team support') + ->addOption('verification', null, InputOption::VALUE_NONE, 'Indicates whether Jetstream should be scaffolded with email verification support') ->addOption('pest', null, InputOption::VALUE_NONE, 'Installs the Pest testing framework') ->addOption('phpunit', null, InputOption::VALUE_NONE, 'Installs the PHPUnit testing framework') + ->addOption('prompt-breeze', null, InputOption::VALUE_NONE, 'Issues a prompt to determine if Breeze should be installed (Deprecated)') + ->addOption('prompt-jetstream', null, InputOption::VALUE_NONE, 'Issues a prompt to determine if Jetstream should be installed (Deprecated)') ->addOption('force', 'f', InputOption::VALUE_NONE, 'Forces install even if the directory already exists'); } @@ -75,11 +85,6 @@ protected function interact(InputInterface $input, OutputInterface $output) ╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝╚═╝ ╚═══╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚═╝ '.PHP_EOL.PHP_EOL); - /*spin( - fn () => sleep(9), - 'Fetching response...' - );*/ - if (! $input->getArgument('name')) { $input->setArgument('name', text( label: 'What is the name of your project?', @@ -91,47 +96,39 @@ protected function interact(InputInterface $input, OutputInterface $output) )); } - if (! $input->getArgument('panel')) { - $input->setArgument('panel', text( - label: 'What is the name of your panel?', - placeholder: 'E.g. admin', - default: 'admin', - required: 'The panel name is required.', - validate: fn ($value) => preg_match('/[^\pL\pN\-_.]/', $value) !== 0 - ? 'The name may only contain letters, numbers, dashes, underscores, and periods.' - : null, - )); + if (! $input->getOption('breeze') && ! $input->getOption('jet')) { + match (select( + label: 'Would you like to install a starter kit?', + options: [ + 'none' => 'No starter kit', + 'breeze' => 'Laravel Breeze', + 'jetstream' => 'Laravel Jetstream', + ], + default: 'none', + )) { + 'breeze' => $input->setOption('breeze', true), + 'jetstream' => $input->setOption('jet', true), + default => null, + }; + } + + if ($input->getOption('breeze')) { + $this->promptForBreezeOptions($input); + } elseif ($input->getOption('jet')) { + $this->promptForJetstreamOptions($input); } if (! $input->getOption('phpunit') && ! $input->getOption('pest')) { $input->setOption('pest', select( - label: 'Which testing framework do you prefer?', - options: ['Pest', 'PHPUnit'], - default: 'Pest', - ) === 'Pest'); + label: 'Which testing framework do you prefer?', + options: ['Pest', 'PHPUnit'], + default: 'Pest', + ) === 'Pest'); } if (! $input->getOption('git') && $input->getOption('github') === false && Process::fromShellCommandline('git --version')->run() === 0) { $input->setOption('git', confirm(label: 'Would you like to initialize a Git repository?', default: false)); } - - if (! $input->getOption('migrate')) { - $input->setOption('migrate', confirm(label: 'Would you like to initialize a Git repository?', default: true)); - } - - if (! $input->getOption('database')) { - $input->setOption('database', select( - label: 'Which database will your application use?', - options: [ - 'mysql' => 'MySQL', - 'mariadb' => 'MariaDB', - 'pgsql' => 'PostgreSQL', - 'sqlite' => 'SQLite', - 'sqlsrv' => 'SQL Server', - ], - default: 'mysql', - ) === 'mysql'); - } } /** @@ -143,6 +140,8 @@ protected function interact(InputInterface $input, OutputInterface $output) */ protected function execute(InputInterface $input, OutputInterface $output): int { + $this->validateStackOption($input); + $name = $input->getArgument('name'); $directory = $name !== '.' ? getcwd().'/'.$name : '.'; @@ -185,8 +184,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $directory.'/.env' ); - $database = $input->getOption('database'); - $migrate = $input->getOption('migrate'); + [$database, $migrate] = $this->promptForDatabaseOptions($directory, $input); $this->configureDefaultDatabaseConnection($directory, $database, $name, $migrate); @@ -201,12 +199,23 @@ protected function execute(InputInterface $input, OutputInterface $output): int $this->createRepository($directory, $input, $output); } - if ($input->getOption('pest')) { + if ($input->getOption('breeze')) { + $this->installBreeze($directory, $input, $output); + } elseif ($input->getOption('jet')) { + $this->installJetstream($directory, $input, $output); + } elseif ($input->getOption('pest')) { $this->installPest($directory, $input, $output); } - $this->installFilament($directory, $input, $output); - $output->writeln(''); + spin( + fn () => $this->installFilament($directory, $input, $output), + 'installing Filament ...' + ); + + if ($input->getOption('github') !== false) { + $this->pushToGitHub($name, $directory, $input, $output); + $output->writeln(''); + } $output->writeln(" INFO Application ready in [{$name}]. You can start your local development using:".PHP_EOL); @@ -214,13 +223,42 @@ protected function execute(InputInterface $input, OutputInterface $output): int $output->writeln('php artisan serve'); $output->writeln(''); - $output->writeln(' New to Filament? Check out our bootcamp and documentation. Build something amazing!'); + $output->writeln(" INFO Login Information=bold>[{$name}]:".PHP_EOL); + $output->writeln("➜ Email Address:admin@{$name}.com"); + $output->writeln('➜ Password:password'); + $output->writeln(''); + + $output->writeln(' New to Filament? Check out our documentation. Build something amazing!'); $output->writeln(''); } return $process->getExitCode(); } + /** + * Install FilamentPHP into the application. + * + * @param string $directory + * @param \Symfony\Component\Console\Input\InputInterface $input + * @param \Symfony\Component\Console\Output\OutputInterface $output + * @return void + */ + protected function installFilament(string $directory, InputInterface $input, OutputInterface $output) + { + $name = $input->getArgument('name'); + + $commands = array_filter([ + $this->findComposer().' require filament/filament', //--quiet + $this->phpBinary().' artisan filament:install --panels',// --no-interaction --quiet + ]); + + $this->runCommands($commands, $input, $output, workingPath: $directory); + + $this->runCommands([$this->phpBinary().' artisan make:filament-user --name=Admin --email="admin@'.$name.'.com" --password=password'], $input, $output, workingPath: $directory); + + $this->commitChanges('Install Filament', $directory, $input, $output); + } + /** * Return the local machine's default Git branch if set or default to `main`. * @@ -301,13 +339,13 @@ protected function configureDefaultDatabaseConnection(string $directory, string } $this->replaceInFile( - 'DB_DATABASE=filament', + 'DB_DATABASE=laravel', 'DB_DATABASE='.str_replace('-', '_', strtolower($name)), $directory.'/.env' ); $this->replaceInFile( - 'DB_DATABASE=filament', + 'DB_DATABASE=laravel', 'DB_DATABASE='.str_replace('-', '_', strtolower($name)), $directory.'/.env.example' ); @@ -329,23 +367,259 @@ public function usingLaravel11OrNewer(string $directory): bool } /** - * Install FilamentPHP into the application. + * Comment the irrelevant database configuration entries for SQLite applications. + * + * @param string $directory + * @return void + */ + protected function commentDatabaseConfigurationForSqlite(string $directory): void + { + $defaults = [ + 'DB_HOST=127.0.0.1', + 'DB_PORT=3306', + 'DB_DATABASE=laravel', + 'DB_USERNAME=root', + 'DB_PASSWORD=', + ]; + + $this->replaceInFile( + $defaults, + collect($defaults)->map(fn ($default) => "# {$default}")->all(), + $directory.'/.env' + ); + + $this->replaceInFile( + $defaults, + collect($defaults)->map(fn ($default) => "# {$default}")->all(), + $directory.'/.env.example' + ); + } + + /** + * Uncomment the relevant database configuration entries for non SQLite applications. + * + * @param string $directory + * @return void + */ + protected function uncommentDatabaseConfiguration(string $directory) + { + $defaults = [ + '# DB_HOST=127.0.0.1', + '# DB_PORT=3306', + '# DB_DATABASE=laravel', + '# DB_USERNAME=root', + '# DB_PASSWORD=', + ]; + + $this->replaceInFile( + $defaults, + collect($defaults)->map(fn ($default) => substr($default, 2))->all(), + $directory.'/.env' + ); + + $this->replaceInFile( + $defaults, + collect($defaults)->map(fn ($default) => substr($default, 2))->all(), + $directory.'/.env.example' + ); + } + + /** + * Install Laravel Breeze into the application. * * @param string $directory * @param \Symfony\Component\Console\Input\InputInterface $input * @param \Symfony\Component\Console\Output\OutputInterface $output * @return void */ - protected function installFilament(string $directory, InputInterface $input, OutputInterface $output) + protected function installBreeze(string $directory, InputInterface $input, OutputInterface $output) { $commands = array_filter([ - $this->findComposer().' require filament/filament', - $this->phpBinary().' artisan filament:install --panels', + $this->findComposer().' require laravel/breeze', + trim(sprintf( + $this->phpBinary().' artisan breeze:install %s %s %s %s %s', + $input->getOption('stack'), + $input->getOption('typescript') ? '--typescript' : '', + $input->getOption('pest') ? '--pest' : '', + $input->getOption('dark') ? '--dark' : '', + $input->getOption('ssr') ? '--ssr' : '', + )), ]); $this->runCommands($commands, $input, $output, workingPath: $directory); - $this->commitChanges('Install Filament', $directory, $input, $output); + $this->commitChanges('Install Breeze', $directory, $input, $output); + } + + /** + * Install Laravel Jetstream into the application. + * + * @param string $directory + * @param \Symfony\Component\Console\Input\InputInterface $input + * @param \Symfony\Component\Console\Output\OutputInterface $output + * @return void + */ + protected function installJetstream(string $directory, InputInterface $input, OutputInterface $output) + { + $commands = array_filter([ + $this->findComposer().' require laravel/jetstream', + trim(sprintf( + $this->phpBinary().' artisan jetstream:install %s %s %s %s %s %s %s', + $input->getOption('stack'), + $input->getOption('api') ? '--api' : '', + $input->getOption('dark') ? '--dark' : '', + $input->getOption('teams') ? '--teams' : '', + $input->getOption('pest') ? '--pest' : '', + $input->getOption('verification') ? '--verification' : '', + $input->getOption('ssr') ? '--ssr' : '', + )), + ]); + + $this->runCommands($commands, $input, $output, workingPath: $directory); + + $this->commitChanges('Install Jetstream', $directory, $input, $output); + } + + /** + * Determine the default database connection. + * + * @param string $directory + * @param \Symfony\Component\Console\Input\InputInterface $input + * @return string + */ + protected function promptForDatabaseOptions(string $directory, InputInterface $input) + { + // Laravel 11.x appliations use SQLite as default... + $defaultDatabase = $this->usingLaravel11OrNewer($directory) ? 'sqlite' : 'mysql'; + + if ($input->isInteractive()) { + $database = select( + label: 'Which database will your application use?', + options: [ + 'mysql' => 'MySQL', + 'mariadb' => 'MariaDB', + 'pgsql' => 'PostgreSQL', + 'sqlite' => 'SQLite', + 'sqlsrv' => 'SQL Server', + ], + default: $defaultDatabase + ); + + //if ($this->usingLaravel11OrNewer($directory) && $database !== $defaultDatabase) { + $migrate = confirm(label: 'Default database updated. Would you like to run the default database migrations?', default: true); + //} + } + + return [$database ?? $defaultDatabase, $migrate ?? false]; + } + + /** + * Determine the stack for Breeze. + * + * @param \Symfony\Component\Console\Input\InputInterface $input + * @return void + */ + protected function promptForBreezeOptions(InputInterface $input) + { + if (! $input->getOption('stack')) { + $input->setOption('stack', select( + label: 'Which Breeze stack would you like to install?', + options: [ + 'blade' => 'Blade with Alpine', + 'livewire' => 'Livewire (Volt Class API) with Alpine', + 'livewire-functional' => 'Livewire (Volt Functional API) with Alpine', + 'react' => 'React with Inertia', + 'vue' => 'Vue with Inertia', + 'api' => 'API only', + ], + default: 'blade', + )); + } + + if (in_array($input->getOption('stack'), ['react', 'vue']) && (! $input->getOption('dark') || ! $input->getOption('ssr'))) { + collect(multiselect( + label: 'Would you like any optional features?', + options: [ + 'dark' => 'Dark mode', + 'ssr' => 'Inertia SSR', + 'typescript' => 'TypeScript (experimental)', + ], + default: array_filter([ + $input->getOption('dark') ? 'dark' : null, + $input->getOption('ssr') ? 'ssr' : null, + $input->getOption('typescript') ? 'typescript' : null, + ]), + ))->each(fn ($option) => $input->setOption($option, true)); + } elseif (in_array($input->getOption('stack'), ['blade', 'livewire', 'livewire-functional']) && ! $input->getOption('dark')) { + $input->setOption('dark', confirm( + label: 'Would you like dark mode support?', + default: false, + )); + } + } + + /** + * Determine the stack for Jetstream. + * + * @param \Symfony\Component\Console\Input\InputInterface $input + * @return void + */ + protected function promptForJetstreamOptions(InputInterface $input) + { + if (! $input->getOption('stack')) { + $input->setOption('stack', select( + label: 'Which Jetstream stack would you like to install?', + options: [ + 'livewire' => 'Livewire', + 'inertia' => 'Vue with Inertia', + ], + default: 'livewire', + )); + } + + collect(multiselect( + label: 'Would you like any optional features?', + options: collect([ + 'api' => 'API support', + 'dark' => 'Dark mode', + 'verification' => 'Email verification', + 'teams' => 'Team support', + ])->when( + $input->getOption('stack') === 'inertia', + fn ($options) => $options->put('ssr', 'Inertia SSR') + )->all(), + default: array_filter([ + $input->getOption('api') ? 'api' : null, + $input->getOption('dark') ? 'dark' : null, + $input->getOption('teams') ? 'teams' : null, + $input->getOption('verification') ? 'verification' : null, + $input->getOption('stack') === 'inertia' && $input->getOption('ssr') ? 'ssr' : null, + ]), + ))->each(fn ($option) => $input->setOption($option, true)); + } + + /** + * Validate the starter kit stack input. + * + * @param \Symfony\Components\Console\Input\InputInterface + */ + protected function validateStackOption(InputInterface $input) + { + if ($input->getOption('breeze')) { + if (! in_array($input->getOption('stack'), $stacks = ['blade', 'livewire', 'livewire-functional', 'react', 'vue', 'api'])) { + throw new \InvalidArgumentException("Invalid Breeze stack [{$input->getOption('stack')}]. Valid options are: ".implode(', ', $stacks).'.'); + } + + return; + } + + if ($input->getOption('jet')) { + if (! in_array($input->getOption('stack'), $stacks = ['inertia', 'livewire'])) { + throw new \InvalidArgumentException("Invalid Jetstream stack [{$input->getOption('stack')}]. Valid options are: ".implode(', ', $stacks).'.'); + } + + return; + } } /** @@ -657,4 +931,4 @@ protected function pregReplaceInFile(string $pattern, string $replace, string $f preg_replace($pattern, $replace, file_get_contents($file)) ); } -} +} \ No newline at end of file diff --git a/src/NewCommandFilament.php b/src/NewCommandFilament.php new file mode 100644 index 0000000..e051a93 --- /dev/null +++ b/src/NewCommandFilament.php @@ -0,0 +1,749 @@ +setName('new') + ->setDescription('Create a new Filament application') + ->addArgument('name', InputArgument::REQUIRED) + //->addArgument('panel', InputArgument::REQUIRED) + ->addOption('dev', null, InputOption::VALUE_NONE, 'Installs the latest "development" release') + ->addOption('migrate', null, InputOption::VALUE_NONE, 'run migration') + ->addOption('database', null, InputOption::VALUE_NONE, 'database type') + ->addOption('git', null, InputOption::VALUE_NONE, 'Initialize a Git repository') + ->addOption('branch', null, InputOption::VALUE_REQUIRED, 'The branch that should be created for a new repository', $this->defaultBranch()) + ->addOption('github', null, InputOption::VALUE_OPTIONAL, 'Create a new repository on GitHub', false) + ->addOption('organization', null, InputOption::VALUE_REQUIRED, 'The GitHub organization to create the new repository for') + ->addOption('pest', null, InputOption::VALUE_NONE, 'Installs the Pest testing framework') + ->addOption('phpunit', null, InputOption::VALUE_NONE, 'Installs the PHPUnit testing framework') + ->addOption('force', 'f', InputOption::VALUE_NONE, 'Forces install even if the directory already exists'); + } + + /** + * Interact with the user before validating the input. + * + * @param \Symfony\Component\Console\Input\InputInterface $input + * @param \Symfony\Component\Console\Output\OutputInterface $output + * @return void + */ + protected function interact(InputInterface $input, OutputInterface $output) + { + parent::interact($input, $output); + + $this->configurePrompts($input, $output); + + $output->write(PHP_EOL.' + ███████╗██╗██╗ █████╗ ███╗ ███╗███████╗███╗ ██╗████████╗██████╗ ██╗ ██╗██████╗ + ██╔════╝██║██║ ██╔══██╗████╗ ████║██╔════╝████╗ ██║╚══██╔══╝██╔══██╗██║ ██║██╔══██╗ + █████╗ ██║██║ ███████║██╔████╔██║█████╗ ██╔██╗ ██║ ██║ ██████╔╝███████║██████╔╝ + ██╔══╝ ██║██║ ██╔══██║██║╚██╔╝██║██╔══╝ ██║╚██╗██║ ██║ ██╔═══╝ ██╔══██║██╔═══╝ + ██║ ██║███████╗██║ ██║██║ ╚═╝ ██║███████╗██║ ╚████║ ██║ ██║ ██║ ██║██║ + ╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝╚═╝ ╚═══╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚═╝ + '.PHP_EOL.PHP_EOL); + + if (! $input->getArgument('name')) { + $input->setArgument('name', text( + label: 'What is the name of your project?', + placeholder: 'E.g. example-app', + required: 'The project name is required.', + validate: fn ($value) => preg_match('/[^\pL\pN\-_.]/', $value) !== 0 + ? 'The name may only contain letters, numbers, dashes, underscores, and periods.' + : null, + )); + } + + /*if (! $input->getArgument('panel')) { + $input->setArgument('panel', text( + label: 'What is the name of your panel?', + placeholder: 'E.g. admin', + default: 'admin', + required: 'The panel name is required.', + validate: fn ($value) => preg_match('/[^\pL\pN\-_.]/', $value) !== 0 + ? 'The name may only contain letters, numbers, dashes, underscores, and periods.' + : null, + )); + }*/ + + if (! $input->getOption('phpunit') && ! $input->getOption('pest')) { + $input->setOption('pest', select( + label: 'Which testing framework do you prefer?', + options: ['Pest', 'PHPUnit'], + default: 'Pest', + ) === 'Pest'); + } + + if (! $input->getOption('git') && $input->getOption('github') === false && Process::fromShellCommandline('git --version')->run() === 0) { + $input->setOption('git', confirm(label: 'Would you like to initialize a Git repository?', default: false)); + } + + if (! $input->getOption('migrate')) { + $input->setOption('migrate', confirm(label: 'Would you like to run migration after the installation completed?', default: true)); + } + + if (! $input->getOption('database')) { + $input->setOption('database', select( + label: 'Which database will your application use?', + options: [ + 'mysql' => 'MySQL', + 'mariadb' => 'MariaDB', + 'pgsql' => 'PostgreSQL', + 'sqlite' => 'SQLite', + 'sqlsrv' => 'SQL Server', + ], + default: 'mysql', + ) === 'mysql'); + } + } + + /** + * Execute the command. + * + * @param \Symfony\Component\Console\Input\InputInterface $input + * @param \Symfony\Component\Console\Output\OutputInterface $output + * @return int + */ + protected function execute(InputInterface $input, OutputInterface $output): int + { + $name = $input->getArgument('name'); + + $directory = $name !== '.' ? getcwd().'/'.$name : '.'; + + $this->composer = new Composer(new Filesystem(), $directory); + + $version = $this->getVersion($input); + + if (! $input->getOption('force')) { + $this->verifyApplicationDoesntExist($directory); + } + + if ($input->getOption('force') && $directory === '.') { + throw new RuntimeException('Cannot use --force option when using current directory for installation!'); + } + + $composer = $this->findComposer(); + + $commands = [ + $composer." create-project laravel/laravel \"$directory\" $version --remove-vcs --prefer-dist",//--quiet + ]; + + if ($directory != '.' && $input->getOption('force')) { + if (PHP_OS_FAMILY == 'Windows') { + array_unshift($commands, "(if exist \"$directory\" rd /s /q \"$directory\")"); + } else { + array_unshift($commands, "rm -rf \"$directory\""); + } + } + + if (PHP_OS_FAMILY != 'Windows') { + $commands[] = "chmod 755 \"$directory/artisan\""; + } + + /*$process = spin( + fn () => $this->runCommands($commands, $input, $output), + 'Installing Laravel Project...' + ); + + \var_dump($process->isSuccessful()); + exit;*/ + + if (($process = $this->runCommands($commands, $input, $output))->isSuccessful()) { + if ($name !== '.') { + $this->replaceInFile( + 'APP_URL=http://localhost', + 'APP_URL='.$this->generateAppUrl($name), + $directory.'/.env' + ); + + $database = $input->getOption('database'); + $migrate = $input->getOption('migrate'); + + spin( + fn () => $this->configureDefaultDatabaseConnection($directory, $database, $name, $migrate), + 'preparing the database ...' + ); + + if ($migrate) { + spin( + fn () => $this->runCommands([ + $this->phpBinary().' artisan migrate', + ], $input, $output, workingPath: $directory), + 'running migration ...' + ); + } + } + + if ($input->getOption('git') || $input->getOption('github') !== false) { + spin( + fn () => $this->createRepository($directory, $input, $output), + 'preparing git ...' + ); + } + + if ($input->getOption('pest')) { + spin( + fn () => $this->installPest($directory, $input, $output), + 'installing pest ...' + ); + } + + spin( + fn () => $this->installFilament($directory, $input, $output), + 'installing Filament ...' + ); + + $output->writeln(''); + + $output->writeln(" INFO Application ready in [{$name}]. You can start your local development using:".PHP_EOL); + + $output->writeln('cd '.$name.''); + $output->writeln('php artisan serve'); + $output->writeln(''); + + $output->writeln(" INFO Login Information=bold>[{$name}]:".PHP_EOL); + $output->writeln("➜ Email Address:admin@{$name}.com"); + $output->writeln('➜ Password:123123123'); + $output->writeln(''); + + $output->writeln(' New to Filament? Check out our documentation. Build something amazing!'); + $output->writeln(''); + } + + return $process->getExitCode(); + } + + /** + * Return the local machine's default Git branch if set or default to `main`. + * + * @return string + */ + protected function defaultBranch() + { + $process = new Process(['git', 'config', '--global', 'init.defaultBranch']); + + $process->run(); + + $output = trim($process->getOutput()); + + return $process->isSuccessful() && $output ? $output : 'main'; + } + + /** + * Configure the default database connection. + * + * @param string $directory + * @param string $database + * @param string $name + * @param bool $migrate + * @return void + */ + protected function configureDefaultDatabaseConnection(string $directory, string $database, string $name, bool $migrate) + { + // MariaDB configuration only exists as of Laravel 11... + if ($database === 'mariadb' && ! $this->usingLaravel11OrNewer($directory)) { + $database = 'mysql'; + } + + $this->pregReplaceInFile( + '/DB_CONNECTION=.*/', + 'DB_CONNECTION='.$database, + $directory.'/.env' + ); + + $this->pregReplaceInFile( + '/DB_CONNECTION=.*/', + 'DB_CONNECTION='.$database, + $directory.'/.env.example' + ); + + if ($database === 'sqlite') { + $environment = file_get_contents($directory.'/.env'); + + // If database options aren't commented, comment them for SQLite... + if (! str_contains($environment, '# DB_HOST=127.0.0.1')) { + $this->commentDatabaseConfigurationForSqlite($directory); + + return; + } + + return; + } + + // Any commented database configuration options should be uncommented when not on SQLite... + $this->uncommentDatabaseConfiguration($directory); + + $defaultPorts = [ + 'pgsql' => '5432', + 'sqlsrv' => '1433', + ]; + + if (isset($defaultPorts[$database])) { + $this->replaceInFile( + 'DB_PORT=3306', + 'DB_PORT='.$defaultPorts[$database], + $directory.'/.env' + ); + + $this->replaceInFile( + 'DB_PORT=3306', + 'DB_PORT='.$defaultPorts[$database], + $directory.'/.env.example' + ); + } + + $this->replaceInFile( + 'DB_DATABASE=laravel', + 'DB_DATABASE='.str_replace('-', '_', strtolower($name)), + $directory.'/.env' + ); + + $this->replaceInFile( + 'DB_DATABASE=laravel', + 'DB_DATABASE='.str_replace('-', '_', strtolower($name)), + $directory.'/.env.example' + ); + } + + /** + * Determine if the application is using Laravel 11 or newer. + * + * @param string $directory + * @return bool + */ + public function usingLaravel11OrNewer(string $directory): bool + { + $version = json_decode(file_get_contents($directory.'/composer.json'), true)['require']['laravel/framework']; + $version = str_replace('^', '', $version); + $version = explode('.', $version)[0]; + + return $version >= 11; + } + + /** + * Install FilamentPHP into the application. + * + * @param string $directory + * @param \Symfony\Component\Console\Input\InputInterface $input + * @param \Symfony\Component\Console\Output\OutputInterface $output + * @return void + */ + protected function installFilament(string $directory, InputInterface $input, OutputInterface $output) + { + $name = $input->getArgument('name'); + + $commands = array_filter([ + $this->findComposer().' require filament/filament', //--quiet + $this->phpBinary().' artisan filament:install --panels --no-interaction',//--quiet + ]); + + $this->runCommands($commands, $input, $output, workingPath: $directory); + + $this->runCommands([$this->phpBinary().' artisan make:filament-user --name=Admin --email="admin@'.$name.'.com" --password=123123123'], $input, $output, workingPath: $directory); + + $this->commitChanges('Install Filament', $directory, $input, $output); + } + + /** + * Install Pest into the application. + * + * @param \Symfony\Component\Console\Input\InputInterface $input + * @param \Symfony\Component\Console\Output\OutputInterface $output + * @return void + */ + protected function installPest(string $directory, InputInterface $input, OutputInterface $output) + { + if ($this->removeComposerPackages(['phpunit/phpunit', '--no-update'], $output, true) + && $this->requireComposerPackages(['pestphp/pest:^2.0', 'pestphp/pest-plugin-laravel:^2.0'], $output, true)) { + $commands = array_filter([ + $this->phpBinary().' ./vendor/bin/pest --init', + ]); + + $this->runCommands($commands, $input, $output, workingPath: $directory, env: [ + 'PEST_NO_SUPPORT' => 'true', + ]); + + $this->replaceFile( + 'pest/Feature.php', + $directory.'/tests/Feature/ExampleTest.php', + ); + + $this->replaceFile( + 'pest/Unit.php', + $directory.'/tests/Unit/ExampleTest.php', + ); + + $this->commitChanges('Install Pest', $directory, $input, $output); + } + } + + /** + * Create a Git repository and commit the base Laravel skeleton. + * + * @param string $directory + * @param \Symfony\Component\Console\Input\InputInterface $input + * @param \Symfony\Component\Console\Output\OutputInterface $output + * @return void + */ + protected function createRepository(string $directory, InputInterface $input, OutputInterface $output) + { + $branch = $input->getOption('branch') ?: $this->defaultBranch(); + + $commands = [ + 'git init -q', + 'git add .', + 'git commit -q -m "Set up a fresh Laravel app"', + "git branch -M {$branch}", + ]; + + $this->runCommands($commands, $input, $output, workingPath: $directory); + } + + /** + * Commit any changes in the current working directory. + * + * @param string $message + * @param string $directory + * @param \Symfony\Component\Console\Input\InputInterface $input + * @param \Symfony\Component\Console\Output\OutputInterface $output + * @return void + */ + protected function commitChanges(string $message, string $directory, InputInterface $input, OutputInterface $output) + { + if (! $input->getOption('git') && $input->getOption('github') === false) { + return; + } + + $commands = [ + 'git add .', + "git commit -q -m \"$message\"", + ]; + + $this->runCommands($commands, $input, $output, workingPath: $directory); + } + + /** + * Create a GitHub repository and push the git log to it. + * + * @param string $name + * @param string $directory + * @param \Symfony\Component\Console\Input\InputInterface $input + * @param \Symfony\Component\Console\Output\OutputInterface $output + * @return void + */ + protected function pushToGitHub(string $name, string $directory, InputInterface $input, OutputInterface $output) + { + $process = new Process(['gh', 'auth', 'status']); + $process->run(); + + if (! $process->isSuccessful()) { + $output->writeln(' WARN Make sure the "gh" CLI tool is installed and that you\'re authenticated to GitHub. Skipping...'.PHP_EOL); + + return; + } + + $name = $input->getOption('organization') ? $input->getOption('organization')."/$name" : $name; + $flags = $input->getOption('github') ?: '--private'; + + $commands = [ + "gh repo create {$name} --source=. --push {$flags}", + ]; + + $this->runCommands($commands, $input, $output, workingPath: $directory, env: ['GIT_TERMINAL_PROMPT' => 0]); + } + + /** + * Verify that the application does not already exist. + * + * @param string $directory + * @return void + */ + protected function verifyApplicationDoesntExist($directory) + { + if ((is_dir($directory) || is_file($directory)) && $directory != getcwd()) { + throw new RuntimeException('Application already exists!'); + } + } + + /** + * Generate a valid APP_URL for the given application name. + * + * @param string $name + * @return string + */ + protected function generateAppUrl($name) + { + $hostname = mb_strtolower($name).'.test'; + + return $this->canResolveHostname($hostname) ? 'http://'.$hostname : 'http://localhost'; + } + + /** + * Determine whether the given hostname is resolvable. + * + * @param string $hostname + * @return bool + */ + protected function canResolveHostname($hostname) + { + return gethostbyname($hostname.'.') !== $hostname.'.'; + } + + /** + * Get the version that should be downloaded. + * + * @param \Symfony\Component\Console\Input\InputInterface $input + * @return string + */ + protected function getVersion(InputInterface $input) + { + if ($input->getOption('dev')) { + return 'dev-master'; + } + + return ''; + } + + /** + * Get the composer command for the environment. + * + * @return string + */ + protected function findComposer() + { + return implode(' ', $this->composer->findComposer()); + } + + /** + * Get the path to the appropriate PHP binary. + * + * @return string + */ + protected function phpBinary() + { + $phpBinary = (new PhpExecutableFinder)->find(false); + + return $phpBinary !== false + ? ProcessUtils::escapeArgument($phpBinary) + : 'php'; + } + + /** + * Install the given Composer Packages into the application. + * + * @return bool + */ + protected function requireComposerPackages(array $packages, OutputInterface $output, bool $asDev = false) + { + return $this->composer->requirePackages($packages, $asDev, $output); + } + + /** + * Remove the given Composer Packages from the application. + * + * @return bool + */ + protected function removeComposerPackages(array $packages, OutputInterface $output, bool $asDev = false) + { + return $this->composer->removePackages($packages, $asDev, $output); + } + + /** + * Run the given commands. + * + * @param array $commands + * @param \Symfony\Component\Console\Input\InputInterface $input + * @param \Symfony\Component\Console\Output\OutputInterface $output + * @param string|null $workingPath + * @param array $env + * @return \Symfony\Component\Process\Process + */ + protected function runCommands($commands, InputInterface $input, OutputInterface $output, string $workingPath = null, array $env = []) + { + if (! $output->isDecorated()) { + $commands = array_map(function ($value) { + if (str_starts_with($value, 'chmod')) { + return $value; + } + + if (str_starts_with($value, 'git')) { + return $value; + } + + return $value.' --no-ansi'; + }, $commands); + } + + if ($input->getOption('quiet')) { + $commands = array_map(function ($value) { + if (str_starts_with($value, 'chmod')) { + return $value; + } + + if (str_starts_with($value, 'git')) { + return $value; + } + + return $value.' --quiet'; + }, $commands); + } + + $process = Process::fromShellCommandline(implode(' && ', $commands), $workingPath, $env, null, null); + + if ('\\' !== DIRECTORY_SEPARATOR && file_exists('/dev/tty') && is_readable('/dev/tty')) { + try { + $process->setTty(true); + } catch (RuntimeException $e) { + $output->writeln(' WARN '.$e->getMessage().PHP_EOL); + } + } + + $process->run(function ($type, $line) use ($output) { + $output->write(' '.$line); + }); + + return $process; + } + + /** + * Replace the given file. + * + * @param string $replace + * @param string $file + * @return void + */ + protected function replaceFile(string $replace, string $file) + { + $stubs = dirname(__DIR__).'/stubs'; + + file_put_contents( + $file, + file_get_contents("$stubs/$replace"), + ); + } + + /** + * Replace the given string in the given file. + * + * @param string|array $search + * @param string|array $replace + * @param string $file + * @return void + */ + protected function replaceInFile(string|array $search, string|array $replace, string $file) + { + file_put_contents( + $file, + str_replace($search, $replace, file_get_contents($file)) + ); + } + + /** + * Replace the given string in the given file using regular expressions. + * + * @param string|array $search + * @param string|array $replace + * @param string $file + * @return void + */ + protected function pregReplaceInFile(string $pattern, string $replace, string $file) + { + file_put_contents( + $file, + preg_replace($pattern, $replace, file_get_contents($file)) + ); + } + + /** + * Comment the irrelevant database configuration entries for SQLite applications. + * + * @param string $directory + * @return void + */ + protected function commentDatabaseConfigurationForSqlite(string $directory): void + { + $defaults = [ + 'DB_HOST=127.0.0.1', + 'DB_PORT=3306', + 'DB_DATABASE=laravel', + 'DB_USERNAME=root', + 'DB_PASSWORD=', + ]; + + $this->replaceInFile( + $defaults, + collect($defaults)->map(fn ($default) => "# {$default}")->all(), + $directory.'/.env' + ); + + $this->replaceInFile( + $defaults, + collect($defaults)->map(fn ($default) => "# {$default}")->all(), + $directory.'/.env.example' + ); + } + + /** + * Uncomment the relevant database configuration entries for non SQLite applications. + * + * @param string $directory + * @return void + */ + protected function uncommentDatabaseConfiguration(string $directory) + { + $defaults = [ + '# DB_HOST=127.0.0.1', + '# DB_PORT=3306', + '# DB_DATABASE=laravel', + '# DB_USERNAME=root', + '# DB_PASSWORD=', + ]; + + $this->replaceInFile( + $defaults, + collect($defaults)->map(fn ($default) => substr($default, 2))->all(), + $directory.'/.env' + ); + + $this->replaceInFile( + $defaults, + collect($defaults)->map(fn ($default) => substr($default, 2))->all(), + $directory.'/.env.example' + ); + } + +}