diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 8a8adb80..f28548a2 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -16,7 +16,7 @@ jobs:
strategy:
fail-fast: true
matrix:
- php: [7.3, 7.4, '8.0', 8.1, 8.2]
+ php: [8.1, 8.2]
name: PHP ${{ matrix.php }}
@@ -44,7 +44,7 @@ jobs:
strategy:
fail-fast: true
matrix:
- php: [7.3, 7.4, '8.0', 8.1, 8.2]
+ php: [8.1, 8.2]
name: PHP ${{ matrix.php }} - Windows
diff --git a/composer.json b/composer.json
index 09c50daf..de12bea9 100644
--- a/composer.json
+++ b/composer.json
@@ -10,13 +10,14 @@
}
],
"require": {
- "php": "^7.3|^8.0",
- "symfony/console": "^4.0|^5.0|^6.0",
- "symfony/process": "^4.2|^5.0|^6.0"
+ "php": "^8.1",
+ "laravel/prompts": "^0.1",
+ "symfony/console": "^6.0",
+ "symfony/process": "^6.0"
},
"require-dev": {
"phpstan/phpstan": "^1.10",
- "phpunit/phpunit": "^8.0|^9.3"
+ "phpunit/phpunit": "^9.3"
},
"bin": [
"bin/laravel"
diff --git a/src/Concerns/ConfiguresPrompts.php b/src/Concerns/ConfiguresPrompts.php
new file mode 100644
index 00000000..6cb7570e
--- /dev/null
+++ b/src/Concerns/ConfiguresPrompts.php
@@ -0,0 +1,132 @@
+isInteractive() || PHP_OS_FAMILY === 'Windows');
+
+ TextPrompt::fallbackUsing(fn (TextPrompt $prompt) => $this->promptUntilValid(
+ fn () => (new SymfonyStyle($input, $output))->ask($prompt->label, $prompt->default ?: null) ?? '',
+ $prompt->required,
+ $prompt->validate,
+ $output
+ ));
+
+ PasswordPrompt::fallbackUsing(fn (PasswordPrompt $prompt) => $this->promptUntilValid(
+ fn () => (new SymfonyStyle($input, $output))->askHidden($prompt->label) ?? '',
+ $prompt->required,
+ $prompt->validate,
+ $output
+ ));
+
+ ConfirmPrompt::fallbackUsing(fn (ConfirmPrompt $prompt) => $this->promptUntilValid(
+ fn () => (new SymfonyStyle($input, $output))->confirm($prompt->label, $prompt->default),
+ $prompt->required,
+ $prompt->validate,
+ $output
+ ));
+
+ SelectPrompt::fallbackUsing(fn (SelectPrompt $prompt) => $this->promptUntilValid(
+ fn () => (new SymfonyStyle($input, $output))->choice($prompt->label, $prompt->options, $prompt->default),
+ false,
+ $prompt->validate,
+ $output
+ ));
+
+ MultiSelectPrompt::fallbackUsing(function (MultiSelectPrompt $prompt) use ($input, $output) {
+ if ($prompt->default !== []) {
+ return $this->promptUntilValid(
+ fn () => (new SymfonyStyle($input, $output))->choice($prompt->label, $prompt->options, implode(',', $prompt->default), true),
+ $prompt->required,
+ $prompt->validate,
+ $output
+ );
+ }
+
+ return $this->promptUntilValid(
+ fn () => collect((new SymfonyStyle($input, $output))->choice(
+ $prompt->label,
+ array_is_list($prompt->options)
+ ? ['None', ...$prompt->options]
+ : ['none' => 'None', ...$prompt->options],
+ 'None',
+ true)
+ )->reject(array_is_list($prompt->options) ? 'None' : 'none')->all(),
+ $prompt->required,
+ $prompt->validate,
+ $output
+ );
+ });
+
+ SuggestPrompt::fallbackUsing(fn (SuggestPrompt $prompt) => $this->promptUntilValid(
+ function () use ($prompt, $input, $output) {
+ $question = new Question($prompt->label, $prompt->default);
+
+ is_callable($prompt->options)
+ ? $question->setAutocompleterCallback($prompt->options)
+ : $question->setAutocompleterValues($prompt->options);
+
+ return (new SymfonyStyle($input, $output))->askQuestion($question);
+ },
+ $prompt->required,
+ $prompt->validate,
+ $output
+ ));
+ }
+
+ /**
+ * Prompt the user until the given validation callback passes.
+ *
+ * @param \Closure $prompt
+ * @param bool|string $required
+ * @param \Closure|null $validate
+ * @param \Symfony\Component\Console\Output\OutputInterface $output
+ * @return mixed
+ */
+ protected function promptUntilValid($prompt, $required, $validate, $output)
+ {
+ while (true) {
+ $result = $prompt();
+
+ if ($required && ($result === '' || $result === [] || $result === false)) {
+ $output->writeln(''.(is_string($required) ? $required : 'Required.').'');
+
+ continue;
+ }
+
+ if ($validate) {
+ $error = $validate($result);
+
+ if (is_string($error) && strlen($error) > 0) {
+ $output->writeln("{$error}");
+
+ continue;
+ }
+ }
+
+ return $result;
+ }
+ }
+}
diff --git a/src/NewCommand.php b/src/NewCommand.php
index d915e008..3c33cf9e 100644
--- a/src/NewCommand.php
+++ b/src/NewCommand.php
@@ -8,12 +8,17 @@
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
-use Symfony\Component\Console\Question\ChoiceQuestion;
-use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Process\Process;
+use function Laravel\Prompts\confirm;
+use function Laravel\Prompts\multiselect;
+use function Laravel\Prompts\select;
+use function Laravel\Prompts\text;
+
class NewCommand extends Command
{
+ use Concerns\ConfiguresPrompts;
+
/**
* Configure the command options.
*
@@ -38,78 +43,85 @@ protected function configure()
->addOption('teams', null, InputOption::VALUE_NONE, 'Indicates whether Jetstream should be scaffolded with team 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')
- ->addOption('prompt-jetstream', null, InputOption::VALUE_NONE, 'Issues a prompt to determine if Jetstream should be installed')
+ ->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');
}
/**
- * Execute the command.
+ * Interact with the user before validating the input.
*
* @param \Symfony\Component\Console\Input\InputInterface $input
* @param \Symfony\Component\Console\Output\OutputInterface $output
- * @return int
+ * @return void
*/
- protected function execute(InputInterface $input, OutputInterface $output)
+ protected function interact(InputInterface $input, OutputInterface $output)
{
- $installBreeze = $input->getOption('breeze') ||
- ($input->getOption('prompt-breeze') && (new SymfonyStyle($input, $output))->confirm('Would you like to install the Laravel Breeze application scaffolding?', false));
-
- $installJetstream = $input->getOption('jet') ||
- ($input->getOption('prompt-jetstream') && (new SymfonyStyle($input, $output))->confirm('Would you like to install the Laravel Jetstream application scaffolding?', false));
-
- if ($installBreeze) {
- $output->write(" ____
- | __ ) _ __ ___ ___ _______
- | _ \| '__/ _ \/ _ \_ / _ \
- | |_) | | | __/ __// / __/
- |____/|_| \___|\___/___\___|>".PHP_EOL.PHP_EOL);
-
- $stack = $this->breezeStack($input, $output);
- $testingFramework = $this->testingFramework($input, $output);
+ parent::interact($input, $output);
- $dark = false;
+ $this->configurePrompts($input, $output);
- if (in_array($stack, ['blade', 'vue', 'react'])) {
- $dark = $input->getOption('dark') === true
- ? (bool) $input->getOption('dark')
- : (new SymfonyStyle($input, $output))->confirm('Would you like to install dark mode support?', false);
- }
-
- $ssr = false;
-
- if (in_array($stack, ['vue', 'react'])) {
- $ssr = $input->getOption('ssr') === true
- ? (bool) $input->getOption('ssr')
- : (new SymfonyStyle($input, $output))->confirm('Would you like to install Inertia SSR support?', false);
- }
- } elseif ($installJetstream) {
- $output->write(PHP_EOL."
- | | |
- |,---.|--- ,---.|--- ,---.,---.,---.,-.-.
- ||---'| `---.| | |---',---|| | |
- `---'`---'`---'`---'`---'` `---'`---^` ' '>".PHP_EOL.PHP_EOL);
-
- $stack = $this->jetstreamStack($input, $output);
- $testingFramework = $this->testingFramework($input, $output);
-
- $teams = $input->getOption('teams') === true
- ? (bool) $input->getOption('teams')
- : (new SymfonyStyle($input, $output))->confirm('Will your application use teams?', false);
-
- $dark = $input->getOption('dark') === true
- ? (bool) $input->getOption('dark')
- : (new SymfonyStyle($input, $output))->confirm('Would you like to install dark mode support?', false);
- } else {
- $output->write(PHP_EOL.' _ _
+ $output->write(PHP_EOL.' _ _
| | | |
| | __ _ _ __ __ ___ _____| |
| | / _` | \'__/ _` \ \ / / _ \ |
| |___| (_| | | | (_| |\ V / __/ |
|______\__,_|_| \__,_| \_/ \___|_|>'.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->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',
+ ],
+ )) {
+ 'breeze' => $input->setOption('breeze', true),
+ 'jetstream' => $input->setOption('jet', true),
+ default => null,
+ };
}
- sleep(1);
+ 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: ['PHPUnit', 'Pest'],
+ ) === 'Pest');
+ }
+
+ if (! $input->getOption('git') && Process::fromShellCommandline('git --version')->run() === 0) {
+ $input->setOption('git', confirm(label: 'Would you like to initialize a Git repository?'));
+ }
+ }
+
+ /**
+ * 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)
+ {
+ $this->validateStackOption($input);
$name = $input->getArgument('name');
@@ -168,10 +180,10 @@ protected function execute(InputInterface $input, OutputInterface $output)
$this->createRepository($directory, $input, $output);
}
- if ($installBreeze) {
- $this->installBreeze($directory, $stack, $testingFramework, $dark, $ssr, $input, $output);
- } elseif ($installJetstream) {
- $this->installJetstream($directory, $stack, $testingFramework, $teams, $dark, $input, $output);
+ 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);
}
@@ -207,15 +219,11 @@ protected function defaultBranch()
* Install Laravel Breeze into the application.
*
* @param string $directory
- * @param string $stack
- * @param string $testingFramework
- * @param bool $dark
- * @param bool $ssr
* @param \Symfony\Component\Console\Input\InputInterface $input
* @param \Symfony\Component\Console\Output\OutputInterface $output
* @return void
*/
- protected function installBreeze(string $directory, string $stack, string $testingFramework, bool $dark, bool $ssr, InputInterface $input, OutputInterface $output)
+ protected function installBreeze(string $directory, InputInterface $input, OutputInterface $output)
{
chdir($directory);
@@ -223,10 +231,10 @@ protected function installBreeze(string $directory, string $stack, string $testi
$this->findComposer().' require laravel/breeze',
trim(sprintf(
'"'.PHP_BINARY.'" artisan breeze:install %s %s %s %s',
- $stack,
- $testingFramework == 'pest' ? '--pest' : '',
- $dark ? '--dark' : '',
- $ssr ? '--ssr' : '',
+ $input->getOption('stack'),
+ $input->getOption('pest') ? '--pest' : '',
+ $input->getOption('dark') ? '--dark' : '',
+ $input->getOption('ssr') ? '--ssr' : '',
)),
]);
@@ -239,15 +247,11 @@ protected function installBreeze(string $directory, string $stack, string $testi
* Install Laravel Jetstream into the application.
*
* @param string $directory
- * @param string $stack
- * @param string $testingFramework
- * @param bool $teams
- * @param bool $dark
* @param \Symfony\Component\Console\Input\InputInterface $input
* @param \Symfony\Component\Console\Output\OutputInterface $output
* @return void
*/
- protected function installJetstream(string $directory, string $stack, string $testingFramework, bool $teams, bool $dark, InputInterface $input, OutputInterface $output)
+ protected function installJetstream(string $directory, InputInterface $input, OutputInterface $output)
{
chdir($directory);
@@ -255,10 +259,10 @@ protected function installJetstream(string $directory, string $stack, string $te
$this->findComposer().' require laravel/jetstream',
trim(sprintf(
'"'.PHP_BINARY.'" artisan jetstream:install %s %s %s %s',
- $stack,
- $teams ? '--teams' : '',
- $dark ? '--dark' : '',
- $testingFramework == 'pest' ? '--pest' : '',
+ $input->getOption('stack'),
+ $input->getOption('teams') ? '--teams' : '',
+ $input->getOption('dark') ? '--dark' : '',
+ $input->getOption('pest') ? '--pest' : '',
)),
]);
@@ -271,87 +275,94 @@ protected function installJetstream(string $directory, string $stack, string $te
* Determine the stack for Breeze.
*
* @param \Symfony\Component\Console\Input\InputInterface $input
- * @param \Symfony\Component\Console\Output\OutputInterface $output
- * @return string
+ * @return void
*/
- protected function breezeStack(InputInterface $input, OutputInterface $output)
+ protected function promptForBreezeOptions(InputInterface $input)
{
- $stacks = [
- 'blade',
- 'react',
- 'vue',
- 'api',
- ];
-
- if ($input->getOption('stack') && in_array($input->getOption('stack'), $stacks)) {
- return $input->getOption('stack');
+ if (! $input->getOption('stack')) {
+ $input->setOption('stack', select(
+ label: 'Which Breeze stack would you like to install?',
+ options: [
+ 'blade' => 'Blade',
+ 'react' => 'React with Inertia',
+ 'vue' => 'Vue with Inertia',
+ 'api' => 'API only',
+ ]
+ ));
}
- $helper = $this->getHelper('question');
-
- $question = new ChoiceQuestion('Which Breeze stack do you prefer?', $stacks);
-
- $output->write(PHP_EOL);
-
- return $helper->ask($input, new SymfonyStyle($input, $output), $question);
+ 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',
+ ],
+ default: array_filter([
+ $input->getOption('dark') ? 'dark' : null,
+ $input->getOption('ssr') ? 'ssr' : null,
+ ]),
+ ))->each(fn ($option) => $input->setOption($option, true));
+ } elseif ($input->getOption('stack') === 'blade' && ! $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
- * @param \Symfony\Component\Console\Output\OutputInterface $output
- * @return string
+ * @return void
*/
- protected function jetstreamStack(InputInterface $input, OutputInterface $output)
+ protected function promptForJetstreamOptions(InputInterface $input)
{
- $stacks = [
- 'livewire',
- 'inertia',
- ];
-
- if ($input->getOption('stack') && in_array($input->getOption('stack'), $stacks)) {
- return $input->getOption('stack');
+ if (! $input->getOption('stack')) {
+ $input->setOption('stack', select(
+ label: 'Which Jetstream stack would you like to install?',
+ options: [
+ 'inertia' => 'Vue with Inertia',
+ 'livewire' => 'Livewire',
+ ]
+ ));
}
- $helper = $this->getHelper('question');
-
- $question = new ChoiceQuestion('Which Jetstream stack do you prefer?', $stacks);
-
- $output->write(PHP_EOL);
-
- return $helper->ask($input, new SymfonyStyle($input, $output), $question);
+ collect(multiselect(
+ label: 'Would you like any optional features?',
+ options: collect([
+ 'teams' => 'Team support',
+ 'dark' => 'Dark mode',
+ ])->when(
+ $input->getOption('stack') === 'inertia',
+ fn ($options) => $options->put('ssr', 'Inertia SSR')
+ )->all(),
+ default: array_filter([
+ $input->getOption('teams') ? 'teams' : null,
+ $input->getOption('dark') ? 'dark' : null,
+ $input->getOption('stack') === 'inertia' && $input->getOption('ssr') ? 'ssr' : null,
+ ]),
+ ))->each(fn ($option) => $input->setOption($option, true));
}
- /**
- * Determine the testing framework for Jetstream.
- *
- * @param \Symfony\Component\Console\Input\InputInterface $input
- * @param \Symfony\Component\Console\Output\OutputInterface $output
- * @return string
- */
- protected function testingFramework(InputInterface $input, OutputInterface $output)
+ protected function validateStackOption(InputInterface $input)
{
- if ($input->getOption('pest')) {
- return 'pest';
- }
+ if ($input->getOption('breeze')) {
+ if (! in_array($input->getOption('stack'), $stacks = ['blade', 'react', 'vue', 'api'])) {
+ throw new \InvalidArgumentException("Invalid Breeze stack [{$input->getOption('stack')}]. Valid options are: ".implode(', ', $stacks).'.');
+ }
- if ($input->getOption('phpunit')) {
- return 'phpunit';
+ return;
}
- $testingFrameworks = [
- 'pest',
- 'phpunit',
- ];
-
- $helper = $this->getHelper('question');
-
- $question = new ChoiceQuestion('Which testing framework do you prefer?', $testingFrameworks);
-
- $output->write(PHP_EOL);
+ 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 $helper->ask($input, new SymfonyStyle($input, $output), $question);
+ return;
+ }
}
/**
diff --git a/tests/NewCommandTest.php b/tests/NewCommandTest.php
index 52c5939d..4ed37aed 100644
--- a/tests/NewCommandTest.php
+++ b/tests/NewCommandTest.php
@@ -27,7 +27,7 @@ public function test_it_can_scaffold_a_new_laravel_app()
$tester = new CommandTester($app->find('new'));
- $statusCode = $tester->execute(['name' => $scaffoldDirectoryName]);
+ $statusCode = $tester->execute(['name' => $scaffoldDirectoryName], ['interactive' => false]);
$this->assertSame(0, $statusCode);
$this->assertDirectoryExists($scaffoldDirectory.'/vendor');